@motherbase/client 1.2.7 → 1.3.1
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/README.md +265 -220
- package/dist/auth.d.ts +29 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +82 -0
- package/dist/auth.js.map +1 -1
- package/dist/cookie.d.ts +13 -0
- package/dist/cookie.d.ts.map +1 -1
- package/dist/cookie.js +59 -5
- package/dist/cookie.js.map +1 -1
- package/dist/http.d.ts +20 -7
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +18 -5
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/dist/postgrest.d.ts +6 -0
- package/dist/postgrest.d.ts.map +1 -1
- package/dist/postgrest.js +6 -0
- package/dist/postgrest.js.map +1 -1
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js +11 -2
- package/dist/realtime.js.map +1 -1
- package/dist/session.d.ts +7 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +8 -1
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +52 -52
package/README.md
CHANGED
|
@@ -1,220 +1,265 @@
|
|
|
1
|
-
# @motherbase/client
|
|
2
|
-
|
|
3
|
-
A **supabase-js-shaped** client SDK for the Motherbase self-hosted backend. It
|
|
4
|
-
keeps the same entry points (`createClient`, `.from()`, `.auth`, `.storage`,
|
|
5
|
-
`.channel()`, `.functions`) and the same `{ data, error }` result convention, so
|
|
6
|
-
supabase-js code reads and reviews the same way here.
|
|
7
|
-
|
|
8
|
-
**Scope, honestly.** For the **data layer**, this is a package swap: change the
|
|
9
|
-
import and the client factory, keep your queries. Every `supabase-js` data call
|
|
10
|
-
in the contract suite is either supported or supported with a stated difference,
|
|
11
|
-
including relational embedding (`select('*, author(*)')`), `.rpc()`, native
|
|
12
|
-
`upsert`, `count`, `csv()`, the full filter set and PostgREST error codes.
|
|
13
|
-
Auth and realtime are close, with named gaps (no PKCE code exchange, no `mfa`
|
|
14
|
-
namespace on the client, no private channels).
|
|
15
|
-
|
|
16
|
-
**Storage is path-addressed too**: `upload('avatars/123/pic.png')` then
|
|
17
|
-
`download('avatars/123/pic.png')` round trips, `list(prefix)` works, and keys are
|
|
18
|
-
traversal-checked. Still missing there: `move`, `copy`, batch signed URLs and
|
|
19
|
-
image transforms, and authorization is ownership plus bucket tokens rather than
|
|
20
|
-
RLS policies on the objects table. See
|
|
21
|
-
[`docs/migrating-from-supabase.md`](../../docs/migrating-from-supabase.md).
|
|
22
|
-
|
|
23
|
-
Do not plan a migration from the paragraph above. Plan it from
|
|
24
|
-
[`COMPATIBILITY.md`](./COMPATIBILITY.md), which is generated from contract tests
|
|
25
|
-
and is exhaustive, per method, with the exact difference for every partial row.
|
|
26
|
-
|
|
27
|
-
Typed, isomorphic (browser, Next.js server & client, Expo / React Native), built
|
|
28
|
-
on global `fetch` with zero runtime dependencies (`socket.io-client` is an
|
|
29
|
-
optional peer dependency, used only for realtime).
|
|
30
|
-
|
|
31
|
-
## Install
|
|
32
|
-
|
|
33
|
-
```bash
|
|
34
|
-
npm i @motherbase/client
|
|
35
|
-
# optional, only if you use realtime:
|
|
36
|
-
npm i socket.io-client
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Quickstart
|
|
40
|
-
|
|
41
|
-
```ts
|
|
42
|
-
import { createClient, localStorageStorage } from '@motherbase/client';
|
|
43
|
-
|
|
44
|
-
const client = createClient(
|
|
45
|
-
'http://localhost:5001',
|
|
46
|
-
'your-anon-key', // optional; sent as an `apikey` header
|
|
47
|
-
{ storage: localStorageStorage() } // omit for in-memory (SSR-safe) sessions
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
await client.hydrate(); // restore a persisted session (optional)
|
|
51
|
-
|
|
52
|
-
// Auth
|
|
53
|
-
await client.auth.signUp({ email: 'me@example.com', password: 'secret123' });
|
|
54
|
-
const { data, error } = await client.auth.signInWithPassword({
|
|
55
|
-
email: 'me@example.com',
|
|
56
|
-
password: 'secret123',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Query builder (awaitable, PostgREST-style)
|
|
60
|
-
const { data: products } = await client
|
|
61
|
-
.from('products')
|
|
62
|
-
.select('id,name,price')
|
|
63
|
-
.lt('price', 50)
|
|
64
|
-
.order('name', { ascending: false })
|
|
65
|
-
.limit(20);
|
|
66
|
-
|
|
67
|
-
// Storage. `uploaded.path` is the key you passed, so later download/remove/
|
|
68
|
-
// signed-URL calls take the same string.
|
|
69
|
-
const { data: uploaded } = await client.storage.from('avatars').upload(`${userId}/me.png`, myBlob, {
|
|
70
|
-
visibility: 'public',
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// Realtime: postgres_changes, broadcast, presence over one channel (Supabase-shaped)
|
|
74
|
-
const room = client
|
|
75
|
-
.channel('room-1', { config: { broadcast: { self: true }, presence: { key: userId } } })
|
|
76
|
-
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: 'room_id=eq.42' }, p => {
|
|
77
|
-
console.log(p.eventType, p.new, p.old);
|
|
78
|
-
})
|
|
79
|
-
.on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
|
|
80
|
-
.on('presence', { event: 'sync' }, () => console.log(room.presenceState()))
|
|
81
|
-
.subscribe();
|
|
82
|
-
|
|
83
|
-
await room.send({ type: 'broadcast', event: 'cursor', payload: { x: 1, y: 2 } });
|
|
84
|
-
await room.track({ online_at: Date.now() });
|
|
85
|
-
|
|
86
|
-
// Edge functions
|
|
87
|
-
const { data: result } = await client.functions.invoke('hello', { body: { name: 'Ada' } });
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
Every data/auth/storage call resolves to `{ data, error }` and **never throws**
|
|
91
|
-
for expected API errors. `error` is `null` on success, otherwise
|
|
92
|
-
`{ message, status?, code? }`.
|
|
93
|
-
|
|
94
|
-
The SDK attaches the access token automatically and revalidates it on a 401
|
|
95
|
-
(single-flight: concurrent requests share one revalidation). Sessions are Better
|
|
96
|
-
Auth bearer sessions, so there is no separate refresh token to rotate: when the
|
|
97
|
-
token is no longer valid the SDK clears the session and emits `SIGNED_OUT`.
|
|
98
|
-
|
|
99
|
-
## API surface (cheatsheet)
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
// Data. Filters: eq neq gt gte lt lte like ilike likeAllOf likeAnyOf ilikeAllOf
|
|
103
|
-
// ilikeAnyOf in is not or filter match textSearch contains containedBy overlaps
|
|
104
|
-
// rangeGt rangeGte rangeLt rangeLte rangeAdjacent
|
|
105
|
-
client.from(table).select(cols).eq(col, v).in(col, [..]).order(col, { ascending, nullsFirst, referencedTable })
|
|
106
|
-
client.from(table).select(cols).limit(n, { referencedTable }).range(from, to).abortSignal(signal)
|
|
107
|
-
client.from(table).select(cols).or('a.eq.1,and(b.gt.2,c.is.null)') // nests, as PostgREST does
|
|
108
|
-
client.from(table).select(cols).not(col, 'eq', v).filter(col, 'isdistinct', v)
|
|
109
|
-
client.from(table).select(cols).textSearch(col, q, { type: 'websearch', config: 'english' })
|
|
110
|
-
client.from(table).select(cols).single() | .maybeSingle() | .returns<T>() | .csv() | .explain()
|
|
111
|
-
client.from(table).insert(row) | .insert([rowA, rowB], { defaultToNull: false })
|
|
112
|
-
client.from(table).upsert(rows, { onConflict, ignoreDuplicates })
|
|
113
|
-
client.from(table).update(values).eq(..) // a filter is required
|
|
114
|
-
client.from(table).delete().eq(..) // a filter is required
|
|
115
|
-
// Every call resolves { data, error, count }. `count` is null unless you ask:
|
|
116
|
-
// .select('*', { count: 'exact', head: true }).
|
|
117
|
-
// Values are quoted for you: .in('name', ['a,b']) is one value, not two.
|
|
118
|
-
// Writes always return their rows: no .select() needed, and no way to opt out.
|
|
119
|
-
// An unbounded select is capped at 100 rows server side: paginate with .range().
|
|
120
|
-
// explain() needs an admin caller; a normal caller gets 403.
|
|
121
|
-
|
|
122
|
-
// Relational embedding. Compiles to a correlated LATERAL subquery inside the
|
|
123
|
-
// same RLS transaction, so embedded rows are policy-filtered too.
|
|
124
|
-
client.from('posts').select('id, author(name), comments(id)') // object / array by FK shape
|
|
125
|
-
client.from('posts').select('id, author!inner(name)') // inner join semantics
|
|
126
|
-
client.from('posts').select('id, comments(count)') // aggregate
|
|
127
|
-
|
|
128
|
-
// Database functions (typed from the generated Database["public"]["Functions"]).
|
|
129
|
-
// Runs in the caller's RLS transaction. SECURITY DEFINER functions are refused
|
|
130
|
-
// unless an operator allowlists them.
|
|
131
|
-
client.rpc(fn, args, { get, head, count })
|
|
132
|
-
|
|
133
|
-
// Auth → { data, error }
|
|
134
|
-
client.auth.signUp({ email, password, username? })
|
|
135
|
-
client.auth.signInWithPassword({ email, password })
|
|
136
|
-
client.auth.signInWithOtp({ email }) // emails a code
|
|
137
|
-
client.auth.verifyOtp({ email, token }) // completes the OTP sign-in
|
|
138
|
-
client.auth.signInWithOAuth({ provider, options: { redirectTo, scopes, skipBrowserRedirect } })
|
|
139
|
-
client.auth.
|
|
140
|
-
client.auth.
|
|
141
|
-
client.auth.
|
|
142
|
-
client.auth.
|
|
143
|
-
client.auth.
|
|
144
|
-
client.auth.
|
|
145
|
-
client.auth.
|
|
146
|
-
client.auth.
|
|
147
|
-
client.auth.
|
|
148
|
-
client.auth.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
client.storage.from(bucket).
|
|
154
|
-
client.storage.from(bucket).
|
|
155
|
-
client.storage.from(bucket).
|
|
156
|
-
client.storage.from(bucket).
|
|
157
|
-
client.storage.from(bucket).
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
.on('
|
|
163
|
-
.on('
|
|
164
|
-
.
|
|
165
|
-
|
|
166
|
-
channel.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
1
|
+
# @motherbase/client
|
|
2
|
+
|
|
3
|
+
A **supabase-js-shaped** client SDK for the Motherbase self-hosted backend. It
|
|
4
|
+
keeps the same entry points (`createClient`, `.from()`, `.auth`, `.storage`,
|
|
5
|
+
`.channel()`, `.functions`) and the same `{ data, error }` result convention, so
|
|
6
|
+
supabase-js code reads and reviews the same way here.
|
|
7
|
+
|
|
8
|
+
**Scope, honestly.** For the **data layer**, this is a package swap: change the
|
|
9
|
+
import and the client factory, keep your queries. Every `supabase-js` data call
|
|
10
|
+
in the contract suite is either supported or supported with a stated difference,
|
|
11
|
+
including relational embedding (`select('*, author(*)')`), `.rpc()`, native
|
|
12
|
+
`upsert`, `count`, `csv()`, the full filter set and PostgREST error codes.
|
|
13
|
+
Auth and realtime are close, with named gaps (no PKCE code exchange, no `mfa`
|
|
14
|
+
namespace on the client, no private channels).
|
|
15
|
+
|
|
16
|
+
**Storage is path-addressed too**: `upload('avatars/123/pic.png')` then
|
|
17
|
+
`download('avatars/123/pic.png')` round trips, `list(prefix)` works, and keys are
|
|
18
|
+
traversal-checked. Still missing there: `move`, `copy`, batch signed URLs and
|
|
19
|
+
image transforms, and authorization is ownership plus bucket tokens rather than
|
|
20
|
+
RLS policies on the objects table. See
|
|
21
|
+
[`docs/migrating-from-supabase.md`](../../docs/migrating-from-supabase.md).
|
|
22
|
+
|
|
23
|
+
Do not plan a migration from the paragraph above. Plan it from
|
|
24
|
+
[`COMPATIBILITY.md`](./COMPATIBILITY.md), which is generated from contract tests
|
|
25
|
+
and is exhaustive, per method, with the exact difference for every partial row.
|
|
26
|
+
|
|
27
|
+
Typed, isomorphic (browser, Next.js server & client, Expo / React Native), built
|
|
28
|
+
on global `fetch` with zero runtime dependencies (`socket.io-client` is an
|
|
29
|
+
optional peer dependency, used only for realtime).
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm i @motherbase/client
|
|
35
|
+
# optional, only if you use realtime:
|
|
36
|
+
npm i socket.io-client
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { createClient, localStorageStorage } from '@motherbase/client';
|
|
43
|
+
|
|
44
|
+
const client = createClient(
|
|
45
|
+
'http://localhost:5001',
|
|
46
|
+
'your-anon-key', // optional; sent as an `apikey` header
|
|
47
|
+
{ storage: localStorageStorage() } // omit for in-memory (SSR-safe) sessions
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
await client.hydrate(); // restore a persisted session (optional)
|
|
51
|
+
|
|
52
|
+
// Auth
|
|
53
|
+
await client.auth.signUp({ email: 'me@example.com', password: 'secret123' });
|
|
54
|
+
const { data, error } = await client.auth.signInWithPassword({
|
|
55
|
+
email: 'me@example.com',
|
|
56
|
+
password: 'secret123',
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Query builder (awaitable, PostgREST-style)
|
|
60
|
+
const { data: products } = await client
|
|
61
|
+
.from('products')
|
|
62
|
+
.select('id,name,price')
|
|
63
|
+
.lt('price', 50)
|
|
64
|
+
.order('name', { ascending: false })
|
|
65
|
+
.limit(20);
|
|
66
|
+
|
|
67
|
+
// Storage. `uploaded.path` is the key you passed, so later download/remove/
|
|
68
|
+
// signed-URL calls take the same string.
|
|
69
|
+
const { data: uploaded } = await client.storage.from('avatars').upload(`${userId}/me.png`, myBlob, {
|
|
70
|
+
visibility: 'public',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Realtime: postgres_changes, broadcast, presence over one channel (Supabase-shaped)
|
|
74
|
+
const room = client
|
|
75
|
+
.channel('room-1', { config: { broadcast: { self: true }, presence: { key: userId } } })
|
|
76
|
+
.on('postgres_changes', { event: '*', schema: 'public', table: 'messages', filter: 'room_id=eq.42' }, p => {
|
|
77
|
+
console.log(p.eventType, p.new, p.old);
|
|
78
|
+
})
|
|
79
|
+
.on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
|
|
80
|
+
.on('presence', { event: 'sync' }, () => console.log(room.presenceState()))
|
|
81
|
+
.subscribe();
|
|
82
|
+
|
|
83
|
+
await room.send({ type: 'broadcast', event: 'cursor', payload: { x: 1, y: 2 } });
|
|
84
|
+
await room.track({ online_at: Date.now() });
|
|
85
|
+
|
|
86
|
+
// Edge functions
|
|
87
|
+
const { data: result } = await client.functions.invoke('hello', { body: { name: 'Ada' } });
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Every data/auth/storage call resolves to `{ data, error }` and **never throws**
|
|
91
|
+
for expected API errors. `error` is `null` on success, otherwise
|
|
92
|
+
`{ message, status?, code? }`.
|
|
93
|
+
|
|
94
|
+
The SDK attaches the access token automatically and revalidates it on a 401
|
|
95
|
+
(single-flight: concurrent requests share one revalidation). Sessions are Better
|
|
96
|
+
Auth bearer sessions, so there is no separate refresh token to rotate: when the
|
|
97
|
+
token is no longer valid the SDK clears the session and emits `SIGNED_OUT`.
|
|
98
|
+
|
|
99
|
+
## API surface (cheatsheet)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// Data. Filters: eq neq gt gte lt lte like ilike likeAllOf likeAnyOf ilikeAllOf
|
|
103
|
+
// ilikeAnyOf in is not or filter match textSearch contains containedBy overlaps
|
|
104
|
+
// rangeGt rangeGte rangeLt rangeLte rangeAdjacent
|
|
105
|
+
client.from(table).select(cols).eq(col, v).in(col, [..]).order(col, { ascending, nullsFirst, referencedTable })
|
|
106
|
+
client.from(table).select(cols).limit(n, { referencedTable }).range(from, to).abortSignal(signal)
|
|
107
|
+
client.from(table).select(cols).or('a.eq.1,and(b.gt.2,c.is.null)') // nests, as PostgREST does
|
|
108
|
+
client.from(table).select(cols).not(col, 'eq', v).filter(col, 'isdistinct', v)
|
|
109
|
+
client.from(table).select(cols).textSearch(col, q, { type: 'websearch', config: 'english' })
|
|
110
|
+
client.from(table).select(cols).single() | .maybeSingle() | .returns<T>() | .csv() | .explain()
|
|
111
|
+
client.from(table).insert(row) | .insert([rowA, rowB], { defaultToNull: false })
|
|
112
|
+
client.from(table).upsert(rows, { onConflict, ignoreDuplicates })
|
|
113
|
+
client.from(table).update(values).eq(..) // a filter is required
|
|
114
|
+
client.from(table).delete().eq(..) // a filter is required
|
|
115
|
+
// Every call resolves { data, error, count }. `count` is null unless you ask:
|
|
116
|
+
// .select('*', { count: 'exact', head: true }).
|
|
117
|
+
// Values are quoted for you: .in('name', ['a,b']) is one value, not two.
|
|
118
|
+
// Writes always return their rows: no .select() needed, and no way to opt out.
|
|
119
|
+
// An unbounded select is capped at 100 rows server side: paginate with .range().
|
|
120
|
+
// explain() needs an admin caller; a normal caller gets 403.
|
|
121
|
+
|
|
122
|
+
// Relational embedding. Compiles to a correlated LATERAL subquery inside the
|
|
123
|
+
// same RLS transaction, so embedded rows are policy-filtered too.
|
|
124
|
+
client.from('posts').select('id, author(name), comments(id)') // object / array by FK shape
|
|
125
|
+
client.from('posts').select('id, author!inner(name)') // inner join semantics
|
|
126
|
+
client.from('posts').select('id, comments(count)') // aggregate
|
|
127
|
+
|
|
128
|
+
// Database functions (typed from the generated Database["public"]["Functions"]).
|
|
129
|
+
// Runs in the caller's RLS transaction. SECURITY DEFINER functions are refused
|
|
130
|
+
// unless an operator allowlists them.
|
|
131
|
+
client.rpc(fn, args, { get, head, count })
|
|
132
|
+
|
|
133
|
+
// Auth → { data, error }
|
|
134
|
+
client.auth.signUp({ email, password, username? })
|
|
135
|
+
client.auth.signInWithPassword({ email, password })
|
|
136
|
+
client.auth.signInWithOtp({ email }) // emails a code
|
|
137
|
+
client.auth.verifyOtp({ email, token }) // completes the OTP sign-in
|
|
138
|
+
client.auth.signInWithOAuth({ provider, options: { redirectTo, scopes, skipBrowserRedirect } })
|
|
139
|
+
client.auth.getSessionFromUrl() // finishes the OAuth redirect, see below
|
|
140
|
+
client.auth.resetPasswordForEmail(email)
|
|
141
|
+
client.auth.resetPassword({ email, otp, password })
|
|
142
|
+
client.auth.updateUser({ data, password, currentPassword })
|
|
143
|
+
client.auth.setSession({ access_token })
|
|
144
|
+
client.auth.signOut()
|
|
145
|
+
client.auth.getUser() // { data: { user } }, revalidates against the server
|
|
146
|
+
client.auth.getSession() // { data: { session } }, local only
|
|
147
|
+
client.auth.refreshSession()
|
|
148
|
+
client.auth.onAuthStateChange((event, session) => {}) // SIGNED_IN | SIGNED_OUT | TOKEN_REFRESHED
|
|
149
|
+
client.auth.deleteUser({ password }) // self-service, needs the backend flow enabled
|
|
150
|
+
|
|
151
|
+
// Storage → { data, error }. `path` is the object key inside the bucket, and it
|
|
152
|
+
// round trips. The file id returned by upload still works anywhere a path does.
|
|
153
|
+
client.storage.from(bucket).upload(path, file, { visibility, upsert })
|
|
154
|
+
client.storage.from(bucket).download(path)
|
|
155
|
+
client.storage.from(bucket).remove([path])
|
|
156
|
+
client.storage.from(bucket).list(prefix, { limit, offset, search, sortBy })
|
|
157
|
+
client.storage.from(bucket).createSignedUrl(path, expiresIn) // expiresIn is ignored
|
|
158
|
+
client.storage.from(bucket).getPublicUrl(path) // sync
|
|
159
|
+
|
|
160
|
+
// Realtime
|
|
161
|
+
client.channel(name, { config: { broadcast: { self, ack }, presence: { key } } })
|
|
162
|
+
.on('postgres_changes', { event, schema, table, filter }, cb)
|
|
163
|
+
.on('broadcast', { event }, cb)
|
|
164
|
+
.on('presence', { event: 'sync' | 'join' | 'leave' }, cb)
|
|
165
|
+
.subscribe(status => {}) // SUBSCRIBED | TIMED_OUT | CLOSED | CHANNEL_ERROR
|
|
166
|
+
channel.send({ type: 'broadcast', event, payload })
|
|
167
|
+
channel.track(state); channel.untrack(); channel.presenceState()
|
|
168
|
+
client.removeChannel(channel)
|
|
169
|
+
|
|
170
|
+
// Edge functions → { data, error }
|
|
171
|
+
client.functions.invoke(name, { body })
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Social sign-in (OAuth)
|
|
175
|
+
|
|
176
|
+
A provider callback is a browser redirect, so it cannot answer the SDK with a
|
|
177
|
+
session the way a password sign-in does. Two calls, not one:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
// 1. Wherever the button lives. Navigates the browser to the backend, which
|
|
181
|
+
// redirects on to the provider.
|
|
182
|
+
await client.auth.signInWithOAuth({
|
|
183
|
+
provider: 'google',
|
|
184
|
+
options: { redirectTo: 'https://app.example.com/auth/callback' },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// 2. On that page (or on app start). Reads the token the backend appended to
|
|
188
|
+
// the redirect, stores the session, and strips it from the address bar.
|
|
189
|
+
const { data, error } = await client.auth.getSessionFromUrl();
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`getSessionFromUrl()` resolves with a null session and no error when there is
|
|
193
|
+
nothing to read, so calling it on every page load is safe.
|
|
194
|
+
|
|
195
|
+
Step 1 is a full-page navigation, not a fetch. That is deliberate: the backend
|
|
196
|
+
sets an OAuth state cookie there and re-checks it on the provider callback, and
|
|
197
|
+
a cookie set on a cross-site fetch response is refused by Safari and Firefox and
|
|
198
|
+
by Chrome in incognito. Navigating makes it a first-party cookie, so the flow
|
|
199
|
+
works in every browser even when your app and your API are on different domains.
|
|
200
|
+
If you are not using the SDK, point the button at the same URL:
|
|
201
|
+
|
|
202
|
+
```
|
|
203
|
+
https://<your-api>/api/auth/sign-in/social/google?callbackURL=https://app.example.com/auth/callback
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
`callbackURL` is validated against the trusted origins, so it has to be an
|
|
207
|
+
origin you allowlisted.
|
|
208
|
+
|
|
209
|
+
Two backend settings decide whether this works at all:
|
|
210
|
+
|
|
211
|
+
- **Callback URL.** Studio, Authentication, Sign In / Providers shows the exact
|
|
212
|
+
URL to register in the provider's console.
|
|
213
|
+
- **CORS allowed origins.** Studio, Authentication settings, Security · CORS.
|
|
214
|
+
If your app is served from a different origin than the API, that origin must
|
|
215
|
+
be listed. Otherwise the browser refuses the cookie the sign-in start sets,
|
|
216
|
+
and the provider callback comes back as `?error=state_mismatch`.
|
|
217
|
+
|
|
218
|
+
## Storage adapters
|
|
219
|
+
|
|
220
|
+
| Runtime | Adapter |
|
|
221
|
+
| -------- | ------------------------------------------------------------- |
|
|
222
|
+
| Browser | `localStorageStorage()` or `cookieStorage()` |
|
|
223
|
+
| SSR/Node | `memoryStorage()` (default), `serverCookieStorage(cookieMethods)` (getAll/setAll, see `@motherbase/ssr` for Next.js helpers), or a custom `{ get, set }` adapter |
|
|
224
|
+
| Expo/RN | custom `{ get, set }` backed by `AsyncStorage` |
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
228
|
+
import { createClient } from '@motherbase/client';
|
|
229
|
+
|
|
230
|
+
const client = createClient(API_URL, ANON_KEY, {
|
|
231
|
+
storage: {
|
|
232
|
+
async get() {
|
|
233
|
+
const raw = await AsyncStorage.getItem('session');
|
|
234
|
+
return raw ? JSON.parse(raw) : null;
|
|
235
|
+
},
|
|
236
|
+
async set(session) {
|
|
237
|
+
if (session) await AsyncStorage.setItem('session', JSON.stringify(session));
|
|
238
|
+
else await AsyncStorage.removeItem('session');
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Compatibility matrix
|
|
245
|
+
|
|
246
|
+
The matrix is **not maintained by hand**. It lives in
|
|
247
|
+
[`COMPATIBILITY.md`](./COMPATIBILITY.md) and is generated by
|
|
248
|
+
`apps/server/tests/sdk-contract.integration.test.ts`, which runs every listed
|
|
249
|
+
supabase-js call against a live Motherbase server and a live Postgres. Each row
|
|
250
|
+
carries the id of the test that produced it.
|
|
251
|
+
|
|
252
|
+
Rows marked `Not supported` assert the current failure behaviour, so the day a
|
|
253
|
+
feature lands its test fails and the document has to be regenerated. That is the
|
|
254
|
+
mechanism that keeps this page from over-claiming (or under-claiming) again. CI
|
|
255
|
+
regenerates the file and fails if the committed copy differs.
|
|
256
|
+
|
|
257
|
+
Regenerate it locally with:
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
npm run test -w apps/server -- sdk-contract
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
More examples live in [`docs/sdk.md`](../../docs/sdk.md). Project Studio also
|
|
264
|
+
exposes a project-specific Connect page with the exact API URL, WebSocket URL and
|
|
265
|
+
project ID to paste into `.env`.
|
package/dist/auth.d.ts
CHANGED
|
@@ -50,6 +50,18 @@ export interface UpdateUserAttributes {
|
|
|
50
50
|
/** Arbitrary profile fields (name, image, or any custom additional field). */
|
|
51
51
|
data?: Record<string, unknown>;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the token an OAuth callback left in the URL fragment and return the URL
|
|
55
|
+
* with it removed, so a reload or a shared link cannot replay it.
|
|
56
|
+
*
|
|
57
|
+
* The provider callback cannot answer the SDK with a JSON body: it is a browser
|
|
58
|
+
* redirect. The backend therefore appends `#access_token=…` to the redirect it
|
|
59
|
+
* issues, the same implicit flow supabase-js reads. Exported for testing.
|
|
60
|
+
*/
|
|
61
|
+
export declare function readAccessTokenFromUrl(href: string): {
|
|
62
|
+
accessToken: string;
|
|
63
|
+
cleanedHref: string;
|
|
64
|
+
} | null;
|
|
53
65
|
export interface Subscription {
|
|
54
66
|
unsubscribe(): void;
|
|
55
67
|
}
|
|
@@ -172,6 +184,23 @@ export declare class AuthClient {
|
|
|
172
184
|
signOut(): Promise<{
|
|
173
185
|
error: MotherbaseResponse<null>['error'];
|
|
174
186
|
}>;
|
|
187
|
+
/**
|
|
188
|
+
* Finish an OAuth / SSO sign-in after the provider redirected back.
|
|
189
|
+
*
|
|
190
|
+
* `signInWithOAuth` only starts the flow; the session is created during the
|
|
191
|
+
* redirect the browser makes to the backend callback, so the SDK has to pick
|
|
192
|
+
* it up from the URL the browser landed on. Call this once on the page you
|
|
193
|
+
* pointed `redirectTo` at (or on app start, which is where supabase-js does
|
|
194
|
+
* the equivalent). Without it the user is signed in on the backend and signed
|
|
195
|
+
* out in your app.
|
|
196
|
+
*
|
|
197
|
+
* Resolves with a null session and no error when there is nothing to read, so
|
|
198
|
+
* it is safe to call on every page load.
|
|
199
|
+
*/
|
|
200
|
+
getSessionFromUrl(href?: string): Promise<MotherbaseResponse<{
|
|
201
|
+
user: User | null;
|
|
202
|
+
session: Session | null;
|
|
203
|
+
}>>;
|
|
175
204
|
/** Fetch the current user from Better Auth (requires a valid session). */
|
|
176
205
|
getUser(): Promise<MotherbaseResponse<{
|
|
177
206
|
user: User | null;
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAEV,kBAAkB,EAElB,kBAAkB,EAClB,OAAO,EACP,IAAI,EACL,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,6BAA6B;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,KAAK,EAAE,MAAM,CAAC;IACd,+FAA+F;IAC/F,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,0BAA0B;IACzC,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE;QACR,kEAAkE;QAClE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,oCAAoC;QACpC,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,6EAA6E;QAC7E,mBAAmB,CAAC,EAAE,OAAO,CAAC;KAC/B,CAAC;CACH;AAED,MAAM,WAAW,oBAAoB;IACnC,mFAAmF;IACnF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uGAAuG;IACvG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC;+DAC2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|