@insforge/sdk 1.3.0 → 1.3.2-razorpay.0

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.
@@ -0,0 +1,946 @@
1
+ # InsForge SDK Reference
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npm install @insforge/sdk
7
+ ```
8
+
9
+ ## Initialize
10
+
11
+ ```javascript
12
+ import { createClient } from "@insforge/sdk";
13
+
14
+ const insforge = createClient({
15
+ baseUrl: "http://localhost:7130",
16
+ anonKey: "your-anon-key",
17
+ });
18
+ ```
19
+
20
+ `createClient()` is for public and user-scoped clients. Use `createAdminClient()` for project-admin API keys.
21
+
22
+ ## Admin Client
23
+
24
+ ```typescript
25
+ import { createAdminClient } from "@insforge/sdk";
26
+
27
+ const admin = createAdminClient({
28
+ baseUrl: "http://localhost:7130",
29
+ apiKey: process.env.INSFORGE_API_KEY!,
30
+ });
31
+ ```
32
+
33
+ Use this only in trusted server code. The admin client sends `apiKey` as the bearer token for every request.
34
+
35
+ ## SSR Auth Mode
36
+
37
+ Use `@insforge/sdk/ssr` for Next.js/SSR. The helpers keep the refresh token server-owned while still making the short-lived access token available to browser-only SDK surfaces such as Storage and Realtime.
38
+
39
+ Default env resolution:
40
+
41
+ - Browser and server: `NEXT_PUBLIC_INSFORGE_URL`, `NEXT_PUBLIC_INSFORGE_ANON_KEY`
42
+
43
+ Explicit `baseUrl` / `anonKey` values win. Missing SSR config throws a clear error.
44
+
45
+ Default cookies:
46
+
47
+ - `insforge_access_token`: `httpOnly: false`, `sameSite: "lax"`, `path: "/"`, expires at the JWT `exp`
48
+ - `insforge_refresh_token`: `httpOnly: true`, `sameSite: "lax"`, `path: "/"`, expires at the JWT `exp`
49
+
50
+ ### `createBrowserClient()`
51
+
52
+ ```typescript
53
+ import { createBrowserClient } from "@insforge/sdk/ssr";
54
+
55
+ const insforge = createBrowserClient({
56
+ refreshUrl: "/api/auth/refresh", // default
57
+ });
58
+ ```
59
+
60
+ The browser client reads the access-token cookie, uses it for Database, Storage, Functions, and Realtime, and calls the refresh route when the access token is missing or near expiry.
61
+
62
+ ### `createServerClient()`
63
+
64
+ ```typescript
65
+ import { cookies } from "next/headers";
66
+ import { createServerClient } from "@insforge/sdk/ssr";
67
+
68
+ const insforge = createServerClient({
69
+ cookies: await cookies(),
70
+ });
71
+ ```
72
+
73
+ The server client reads only the access-token cookie and passes it as the per-request bearer token.
74
+
75
+ ### `createRefreshAuthRouter()`
76
+
77
+ ```typescript
78
+ // app/api/auth/refresh/route.ts
79
+ import { createRefreshAuthRouter } from "@insforge/sdk/ssr";
80
+
81
+ export const { POST } = createRefreshAuthRouter();
82
+ ```
83
+
84
+ For server-owned refresh cookies, sign-in should also run through a Route Handler or Server Action that can set cookies:
85
+
86
+ ```typescript
87
+ import { NextResponse } from "next/server";
88
+ import { createServerClient, setAuthCookies } from "@insforge/sdk/ssr";
89
+
90
+ export async function POST(request: Request) {
91
+ const client = createServerClient();
92
+ const { data, error } = await client.auth.signInWithPassword(
93
+ await request.json(),
94
+ );
95
+ if (error || !data?.accessToken) {
96
+ return Response.json(error, { status: error?.statusCode ?? 400 });
97
+ }
98
+
99
+ const response = NextResponse.json({
100
+ accessToken: data.accessToken,
101
+ user: data.user,
102
+ });
103
+ setAuthCookies(response.cookies, {
104
+ accessToken: data.accessToken,
105
+ refreshToken: data.refreshToken,
106
+ });
107
+
108
+ return response;
109
+ }
110
+ ```
111
+
112
+ Use `refreshAuth()` directly when the route needs app-specific logic:
113
+
114
+ ```typescript
115
+ import { refreshAuth } from "@insforge/sdk/ssr";
116
+
117
+ export async function POST(request: Request) {
118
+ await beforeRefresh(request);
119
+ const result = await refreshAuth({ request });
120
+ await afterRefresh(result);
121
+ return result.response;
122
+ }
123
+ ```
124
+
125
+ ### `updateSession()`
126
+
127
+ ```typescript
128
+ // proxy.ts on Next.js 16+, middleware.ts on Next.js 15 and earlier
129
+ import { NextResponse, type NextRequest } from "next/server";
130
+ import { updateSession } from "@insforge/sdk/ssr";
131
+
132
+ export async function proxy(request: NextRequest) {
133
+ const response = NextResponse.next({ request });
134
+
135
+ await updateSession({
136
+ requestCookies: request.cookies,
137
+ responseCookies: response.cookies,
138
+ });
139
+
140
+ return response;
141
+ }
142
+ ```
143
+
144
+ ## OAuth Auto-Detection (Browser)
145
+
146
+ The SDK automatically detects and handles OAuth callback parameters when initialized. This feature works seamlessly with the InsForge backend OAuth flow.
147
+
148
+ **How it works:**
149
+
150
+ 1. User calls `signInWithOAuth()` and is redirected to OAuth provider
151
+ 2. After authentication, InsForge redirects back to your app with an `insforge_code` in the URL
152
+ 3. SDK automatically exchanges that code for a session on initialization
153
+ 4. Session is saved and the URL is cleaned - no manual handling needed
154
+
155
+ **Example:**
156
+
157
+ ```javascript
158
+ // Just initialize the client - OAuth is handled automatically
159
+ const insforge = createClient({
160
+ baseUrl: "http://localhost:7130",
161
+ });
162
+
163
+ // If the URL contains OAuth callback parameters like:
164
+ // ?insforge_code=...
165
+ // The SDK will:
166
+ // - Exchange the code for a session
167
+ // - Save the session in memory
168
+ // - Set the auth token for API calls
169
+ // - Clean the URL
170
+
171
+ // You can then immediately use authenticated methods:
172
+ const { data } = await insforge.auth.getCurrentUser();
173
+ ```
174
+
175
+ ## Auth Methods
176
+
177
+ ### `signUp()`
178
+
179
+ ```javascript
180
+ await insforge.auth.signUp({
181
+ email: "user@example.com",
182
+ password: "password123",
183
+ name: "John Doe", // optional
184
+ redirectTo: "http://localhost:3000/sign-in", // optional, recommended for link-based verification
185
+ });
186
+ // Response: { data: { user, accessToken }, error }
187
+ // user: { id, email, name, emailVerified, createdAt, updatedAt }
188
+ // accessToken: JWT token string
189
+ ```
190
+
191
+ If the backend uses link-based email verification, the emailed link opens:
192
+
193
+ ```text
194
+ GET /api/auth/email/verify-link?token=...
195
+ ```
196
+
197
+ InsForge validates the token first, then redirects the browser to your `redirectTo` URL.
198
+ Recommended: use your sign-in page as `redirectTo`, then show a success message and ask the user to sign in with email and password.
199
+
200
+ ### `signInWithPassword()`
201
+
202
+ ```javascript
203
+ await insforge.auth.signInWithPassword({
204
+ email: "user@example.com",
205
+ password: "password123",
206
+ });
207
+ // Response: { data: { user, accessToken }, error }
208
+ // user: { id, email, name, emailVerified, createdAt, updatedAt }
209
+ // accessToken: JWT token string
210
+ ```
211
+
212
+ ### `signInWithOAuth()`
213
+
214
+ ```javascript
215
+ await insforge.auth.signInWithOAuth("google", {
216
+ redirectTo: "http://localhost:3000/dashboard",
217
+ additionalParams: { prompt: "select_account" }, // optional provider-specific OAuth params
218
+ skipBrowserRedirect: true, // optional, returns URL instead of redirecting
219
+ });
220
+ // Response: { data: { url, provider }, error }
221
+ // Auto-redirects in browser unless skipBrowserRedirect: true
222
+ // additionalParams is for provider-specific hints only. Do not pass client_id, scope,
223
+ // redirect_uri, code_challenge, state, or response_type; InsForge sets those server-side
224
+ // and ignores colliding client-provided keys.
225
+
226
+ // AUTOMATIC OAuth Callback Detection (v0.0.14+):
227
+ // When users are redirected back from OAuth provider, the SDK automatically:
228
+ // 1. Detects insforge_code in the URL
229
+ // 2. Exchanges the code for a session
230
+ // 3. Saves the session in memory
231
+ // 4. Cleans the URL
232
+ // No manual handling needed - just initialize the client!
233
+ ```
234
+
235
+ ### `signOut()`
236
+
237
+ ```javascript
238
+ await insforge.auth.signOut();
239
+ // Response: { error }
240
+ // Clears stored tokens
241
+ ```
242
+
243
+ ### `getCurrentUser()`
244
+
245
+ ```javascript
246
+ await insforge.auth.getCurrentUser();
247
+ // Response: { data: { user }, error }
248
+ // user: { id, email, emailVerified, providers, createdAt, updatedAt, profile, metadata }
249
+ // Returns null if not authenticated
250
+ ```
251
+
252
+ For browser apps, call `getCurrentUser()` during startup. The SDK will use the httpOnly refresh cookie automatically when it can refresh the session.
253
+
254
+ For SSR apps, use `@insforge/sdk/ssr`.
255
+
256
+ ### `getProfile()`
257
+
258
+ ```javascript
259
+ await insforge.auth.getProfile(userId);
260
+ // Response: { data: profile, error }
261
+ // profile: { id, nickname, avatar_url, bio, birthday, ... }
262
+ // Gets any user's profile from users table
263
+ ```
264
+
265
+ ### `setProfile()`
266
+
267
+ ```javascript
268
+ await insforge.auth.setProfile({
269
+ nickname: "JohnDoe",
270
+ avatar_url: "https://...",
271
+ bio: "Software developer",
272
+ birthday: "1990-01-01",
273
+ });
274
+ // Response: { data: profile, error }
275
+ // Updates current user's profile in users table
276
+ ```
277
+
278
+ ### `getPublicAuthConfig()`
279
+
280
+ ```javascript
281
+ await insforge.auth.getPublicAuthConfig();
282
+ // Response: { data: GetPublicAuthConfigResponse, error }
283
+ // data: both OAuth providers and email authentication settings in one request
284
+ // This is a public endpoint that doesn't require authentication
285
+ ```
286
+
287
+ ### `resendVerificationEmail()`
288
+
289
+ ```javascript
290
+ await insforge.auth.resendVerificationEmail({
291
+ email: "user@example.com",
292
+ redirectTo: "http://localhost:3000/sign-in", // optional, recommended for link-based verification
293
+ });
294
+ // Response: { data: { success, message }, error }
295
+ ```
296
+
297
+ ### `verifyEmail()`
298
+
299
+ ```javascript
300
+ await insforge.auth.verifyEmail({
301
+ email: "user@example.com",
302
+ otp: "123456",
303
+ });
304
+ // Response: { data: { user, accessToken, csrfToken?, refreshToken? }, error }
305
+ // POST /api/auth/email/verify is code-only
306
+ // Browser link verification uses GET /api/auth/email/verify-link
307
+ // Verification redirect params:
308
+ // - insforge_status=success|error
309
+ // - insforge_type=verify_email
310
+ // - insforge_error (only on error)
311
+ ```
312
+
313
+ ### `sendResetPasswordEmail()`
314
+
315
+ ```javascript
316
+ await insforge.auth.sendResetPasswordEmail({
317
+ email: "user@example.com",
318
+ redirectTo: "http://localhost:3000/reset-password", // optional, recommended for link-based reset
319
+ });
320
+ // Response: { data: { success, message }, error }
321
+ ```
322
+
323
+ ### `exchangeResetPasswordToken()`
324
+
325
+ ```javascript
326
+ await insforge.auth.exchangeResetPasswordToken({
327
+ email: "user@example.com",
328
+ code: "123456",
329
+ });
330
+ // Response: { data: { token, expiresAt }, error }
331
+ ```
332
+
333
+ ### `resetPassword()`
334
+
335
+ ```javascript
336
+ await insforge.auth.resetPassword({
337
+ newPassword: "newSecurePassword123",
338
+ otp: "reset-token",
339
+ });
340
+ // Response: { data: { message }, error }
341
+ // Browser reset links use GET /api/auth/email/reset-password-link first,
342
+ // then your app submits the new password with POST /api/auth/email/reset-password.
343
+ // Reset redirect params:
344
+ // - token (present only when ready)
345
+ // - insforge_status=ready|error
346
+ // - insforge_type=reset_password
347
+ // - insforge_error (only on error)
348
+ ```
349
+
350
+ ## Error Handling
351
+
352
+ ### Auth/Storage/AI Errors (InsForgeError)
353
+
354
+ ```javascript
355
+ {
356
+ error: {
357
+ statusCode: 401,
358
+ error: 'INVALID_CREDENTIALS',
359
+ message: 'Invalid login credentials',
360
+ nextActions: 'Check email and password'
361
+ }
362
+ }
363
+ ```
364
+
365
+ ### Database Errors (PostgrestError)
366
+
367
+ ```javascript
368
+ {
369
+ error: {
370
+ code: 'PGRST116', // PostgreSQL/PostgREST error code
371
+ message: 'JSON object requested, multiple (or no) rows returned',
372
+ details: 'The result contains 5 rows',
373
+ hint: null
374
+ }
375
+ }
376
+ ```
377
+
378
+ ## Auth Session Storage
379
+
380
+ - **Browser**: in-memory (per client instance)
381
+ - **Node.js**: in-memory (per request/client instance)
382
+
383
+ ## Payments Methods
384
+
385
+ Payments methods are provider-scoped and intended for generated app frontends. They call runtime-safe backend routes using the current user token or anon key. Admin-only key, catalog, sync, transaction, and webhook configuration APIs are intentionally not exposed through this frontend SDK surface.
386
+
387
+ ### `stripe.createCheckoutSession()`
388
+
389
+ ```javascript
390
+ const { data, error } = await insforge.payments.stripe.createCheckoutSession(
391
+ "test",
392
+ {
393
+ mode: "payment",
394
+ lineItems: [{ priceId: "price_123", quantity: 1 }],
395
+ successUrl: "https://example.com/success",
396
+ cancelUrl: "https://example.com/pricing",
397
+ idempotencyKey: "cart_123", // optional, recommended for retry-safe checkout creation
398
+ },
399
+ );
400
+
401
+ if (!error && data?.checkoutSession.url) {
402
+ window.location.assign(data.checkoutSession.url);
403
+ }
404
+ ```
405
+
406
+ For one-time payments, `subject` is optional. For subscription checkout, `subject` is required because subscriptions represent ongoing entitlement for an app-defined billing owner.
407
+
408
+ ```javascript
409
+ await insforge.payments.stripe.createCheckoutSession("test", {
410
+ mode: "subscription",
411
+ subject: { type: "team", id: "team_123" },
412
+ lineItems: [{ priceId: "price_monthly_123", quantity: 1 }],
413
+ successUrl: "https://example.com/billing/success",
414
+ cancelUrl: "https://example.com/billing",
415
+ });
416
+ ```
417
+
418
+ ### `stripe.createCustomerPortalSession()`
419
+
420
+ ```javascript
421
+ const { data, error } =
422
+ await insforge.payments.stripe.createCustomerPortalSession("test", {
423
+ subject: { type: "team", id: "team_123" },
424
+ returnUrl: "https://example.com/billing",
425
+ });
426
+
427
+ if (!error && data?.customerPortalSession.url) {
428
+ window.location.assign(data.customerPortalSession.url);
429
+ }
430
+ ```
431
+
432
+ Customer portal sessions require an authenticated user and an existing Stripe customer mapping for the billing subject.
433
+
434
+ ### `razorpay.createOrder()`
435
+
436
+ Razorpay uses Checkout.js instead of a hosted redirect URL. Create an order through InsForge, pass `checkoutOptions` into Razorpay Checkout.js, then verify the signed payment response.
437
+
438
+ ```javascript
439
+ const { data, error } = await insforge.payments.razorpay.createOrder("test", {
440
+ amount: 200000,
441
+ currency: "INR",
442
+ receipt: "cart_123",
443
+ subject: { type: "team", id: "team_123" },
444
+ customerName: "Ada Lovelace",
445
+ customerEmail: "ada@example.com",
446
+ });
447
+
448
+ if (!error && data) {
449
+ const checkout = new Razorpay({
450
+ ...data.checkoutOptions,
451
+ handler: async (response) => {
452
+ await insforge.payments.razorpay.verifyOrder("test", {
453
+ orderId: response.razorpay_order_id,
454
+ paymentId: response.razorpay_payment_id,
455
+ signature: response.razorpay_signature,
456
+ });
457
+ },
458
+ });
459
+
460
+ checkout.open();
461
+ }
462
+ ```
463
+
464
+ ### `razorpay.createSubscription()`
465
+
466
+ ```javascript
467
+ const { data, error } = await insforge.payments.razorpay.createSubscription(
468
+ "test",
469
+ {
470
+ planId: "plan_123",
471
+ totalCount: 12,
472
+ subject: { type: "team", id: "team_123" },
473
+ customerName: "Ada Lovelace",
474
+ customerEmail: "ada@example.com",
475
+ },
476
+ );
477
+
478
+ if (!error && data) {
479
+ const checkout = new Razorpay({
480
+ ...data.checkoutOptions,
481
+ handler: async (response) => {
482
+ await insforge.payments.razorpay.verifySubscription("test", {
483
+ subscriptionId: response.razorpay_subscription_id,
484
+ paymentId: response.razorpay_payment_id,
485
+ signature: response.razorpay_signature,
486
+ });
487
+ },
488
+ });
489
+
490
+ checkout.open();
491
+ }
492
+ ```
493
+
494
+ ### `razorpay.cancelSubscription()`
495
+
496
+ ```javascript
497
+ await insforge.payments.razorpay.cancelSubscription("test", "sub_123", {
498
+ cancelAtCycleEnd: false,
499
+ });
500
+ ```
501
+
502
+ ### `razorpay.pauseSubscription()` / `razorpay.resumeSubscription()`
503
+
504
+ ```javascript
505
+ await insforge.payments.razorpay.pauseSubscription("test", "sub_123");
506
+ await insforge.payments.razorpay.resumeSubscription("test", "sub_123");
507
+ ```
508
+
509
+ Razorpay webhook setup is manual in the Razorpay dashboard. Configure keys and copy the webhook URL, secret, and recommended events from the InsForge payments settings UI.
510
+
511
+ ## Database Methods
512
+
513
+ **Note:** Database operations use [@supabase/postgrest-js](https://github.com/supabase/postgrest-js) under the hood, providing full PostgREST compatibility including advanced features like OR conditions, complex joins, and aggregations.
514
+
515
+ ### `from()`
516
+
517
+ Create a query builder for a table:
518
+
519
+ ```javascript
520
+ const query = insforge.database.from("posts");
521
+ // Returns a PostgREST query builder with all Supabase features
522
+ ```
523
+
524
+ ### SELECT Operations
525
+
526
+ ```javascript
527
+ // Basic select
528
+ await insforge.database.from("posts").select(); // Default: '*'
529
+
530
+ // Select specific columns
531
+ await insforge.database.from("posts").select("id, title, created_at");
532
+
533
+ // With filters
534
+ await insforge.database
535
+ .from("posts")
536
+ .select()
537
+ .eq("user_id", "123")
538
+ .order("created_at", { ascending: false })
539
+ .limit(10);
540
+
541
+ // With joins (PostgREST syntax)
542
+ await insforge.database.from("posts").select("*, users!inner(*)"); // Inner join with users table
543
+
544
+ // Join with specific columns
545
+ await insforge.database
546
+ .from("posts")
547
+ .select("id, title, users(nickname, avatar_url)");
548
+
549
+ // Aliased joins
550
+ await insforge.database.from("posts").select("*, author:users(*)"); // Alias users as author
551
+ // Response: { data: [...], error }
552
+ ```
553
+
554
+ ### INSERT Operations
555
+
556
+ ```javascript
557
+ // Single record - use .select() to return inserted data
558
+ await insforge.database
559
+ .from("posts")
560
+ .insert({ title: "Hello", content: "World" })
561
+ .select();
562
+
563
+ // Multiple records
564
+ await insforge.database
565
+ .from("posts")
566
+ .insert([
567
+ { title: "Post 1", content: "Content 1" },
568
+ { title: "Post 2", content: "Content 2" },
569
+ ])
570
+ .select();
571
+
572
+ // Upsert
573
+ await insforge.database
574
+ .from("posts")
575
+ .upsert({ id: "123", title: "Updated or New" })
576
+ .select();
577
+ // Response: { data: [...], error }
578
+
579
+ // Note: Without .select(), mutations return { data: null, error }
580
+ ```
581
+
582
+ ### UPDATE Operations
583
+
584
+ ```javascript
585
+ await insforge.database
586
+ .from("posts")
587
+ .update({ title: "Updated Title" })
588
+ .eq("id", "123")
589
+ .select();
590
+ // Response: { data: [...], error }
591
+ ```
592
+
593
+ ### DELETE Operations
594
+
595
+ ```javascript
596
+ await insforge.database.from("posts").delete().eq("id", "123").select();
597
+ // Response: { data: [...], error }
598
+ ```
599
+
600
+ ### Filter Methods
601
+
602
+ ```javascript
603
+ .eq('column', value) // Equals
604
+ .neq('column', value) // Not equals
605
+ .gt('column', value) // Greater than
606
+ .gte('column', value) // Greater than or equal
607
+ .lt('column', value) // Less than
608
+ .lte('column', value) // Less than or equal
609
+ .like('column', '%pattern%') // Pattern match (case-sensitive)
610
+ .ilike('column', '%pattern%') // Pattern match (case-insensitive)
611
+ .is('column', null) // IS NULL / IS boolean
612
+ .in('column', [1, 2, 3]) // IN array
613
+
614
+ // Logical operators (v0.0.22+)
615
+ .or('status.eq.active,status.eq.pending') // OR condition
616
+ .and('price.gte.100,price.lte.500') // Explicit AND
617
+ .not('deleted', 'is.true') // NOT condition
618
+ ```
619
+
620
+ #### OR Condition Examples
621
+
622
+ ```javascript
623
+ // Simple OR: status = 'active' OR status = 'pending'
624
+ await insforge.database
625
+ .from("posts")
626
+ .select()
627
+ .or("status.eq.active,status.eq.pending");
628
+
629
+ // OR with other filters (implicit AND)
630
+ await insforge.database
631
+ .from("posts")
632
+ .select()
633
+ .eq("user_id", "123") // AND
634
+ .or("status.eq.draft,status.eq.published"); // OR
635
+
636
+ // Complex OR with NOT
637
+ await insforge.database.from("users").select().or("age.lt.18,age.gt.65");
638
+ // age < 18 OR age > 65
639
+
640
+ // Combining AND and OR
641
+ await insforge.database
642
+ .from("products")
643
+ .select()
644
+ .eq("category", "electronics")
645
+ .or("price.lt.100,rating.gte.4.5");
646
+ // category = 'electronics' AND (price < 100 OR rating >= 4.5)
647
+ ```
648
+
649
+ ### Modifiers
650
+
651
+ ```javascript
652
+ .order('column', { ascending: false }) // Order by
653
+ .limit(10) // Limit results
654
+ .offset(20) // Skip results
655
+ .range(0, 9) // Get specific range
656
+ .single() // Return single object
657
+ .maybeSingle() // Return single object or null
658
+ ```
659
+
660
+ ### Count Options
661
+
662
+ Use with `select()` to get counts:
663
+
664
+ ```javascript
665
+ // Get exact count with data
666
+ const { data, count, error } = await insforge.database
667
+ .from("posts")
668
+ .select("*", { count: "exact" });
669
+
670
+ // Get count without data (HEAD request)
671
+ const { count, error } = await insforge.database
672
+ .from("posts")
673
+ .select("*", { count: "exact", head: true });
674
+
675
+ // Count strategies:
676
+ // 'exact' - Accurate but slower for large tables
677
+ // 'planned' - Fast estimate from query planner
678
+ // 'estimated' - Very fast but rough estimate
679
+ ```
680
+
681
+ ### Method Chaining
682
+
683
+ All methods return the query builder for chaining:
684
+
685
+ ```javascript
686
+ const { data, error } = await insforge.database
687
+ .from("posts")
688
+ .select("id, title, content")
689
+ .eq("status", "published")
690
+ .gte("likes", 100)
691
+ .order("created_at", { ascending: false })
692
+ .limit(10);
693
+
694
+ // With count (Supabase-style)
695
+ const { data, error, count } = await insforge.database
696
+ .from("posts")
697
+ .select("*", { count: "exact" }) // Request exact count
698
+ .eq("status", "published")
699
+ .range(0, 9); // Get first 10
700
+ // Returns: data (array), error (PostgrestError), count (number)
701
+
702
+ // Count without data (head request)
703
+ const { count, error } = await insforge.database
704
+ .from("posts")
705
+ .select("*", { count: "exact", head: true })
706
+ .eq("status", "published");
707
+ // Returns only count, no data
708
+ ```
709
+
710
+ ## Storage Methods
711
+
712
+ ### `storage.from()`
713
+
714
+ ```javascript
715
+ const bucket = insforge.storage.from("avatars");
716
+ // Returns StorageBucket instance for file operations
717
+ ```
718
+
719
+ ### `bucket.upload()`
720
+
721
+ ```javascript
722
+ await bucket.upload("path/file.jpg", file);
723
+ // Response: { data: StorageFileSchema, error }
724
+ // data: { bucket, key, size, mimeType, uploadedAt, url }
725
+ ```
726
+
727
+ ### `bucket.uploadAuto()`
728
+
729
+ ```javascript
730
+ await bucket.uploadAuto(file);
731
+ // Response: { data: StorageFileSchema, error }
732
+ // Auto-generates unique filename
733
+ ```
734
+
735
+ ### `bucket.download()`
736
+
737
+ ```javascript
738
+ await bucket.download("path/file.jpg");
739
+ // Response: { data: Blob, error }
740
+ ```
741
+
742
+ ### `bucket.list()`
743
+
744
+ ```javascript
745
+ await bucket.list({ prefix: "users/", limit: 10 });
746
+ // Response: { data: ListObjectsResponseSchema, error }
747
+ // data: { bucketName, objects[], pagination }
748
+ ```
749
+
750
+ ### `bucket.remove()`
751
+
752
+ ```javascript
753
+ await bucket.remove("path/file.jpg");
754
+ // Response: { data: { message }, error }
755
+ ```
756
+
757
+ ### `bucket.getPublicUrl()`
758
+
759
+ ```javascript
760
+ bucket.getPublicUrl("path/file.jpg");
761
+ // Returns: string URL (no API call)
762
+ ```
763
+
764
+ ## AI Methods
765
+
766
+ ### `ai.chat.completions.create()`
767
+
768
+ Create AI chat completions with support for both streaming and non-streaming responses.
769
+
770
+ #### Non-Streaming
771
+
772
+ ```javascript
773
+ const completion = await insforge.ai.chat.completions.create({
774
+ model: "anthropic/claude-3.5-haiku",
775
+ messages: [
776
+ { role: "system", content: "You are a helpful assistant" },
777
+ { role: "user", content: "Hello, how are you?" },
778
+ ],
779
+ temperature: 0.7,
780
+ maxTokens: 500,
781
+ });
782
+ // Returns an OpenAI-like completion object directly (not a { data, error } envelope):
783
+ // completion.choices[0].message.content - the complete AI response text
784
+ // completion.usage - token usage information
785
+ // completion.model - the model used for generation
786
+ console.log(completion.choices[0].message.content);
787
+ ```
788
+
789
+ #### Streaming
790
+
791
+ ```javascript
792
+ // Returns an async iterable of OpenAI-like chunks for real-time streaming
793
+ const stream = await insforge.ai.chat.completions.create({
794
+ model: "anthropic/claude-3.5-haiku",
795
+ messages: [{ role: "user", content: "Tell me a story" }],
796
+ stream: true,
797
+ });
798
+
799
+ // Each chunk carries an incremental delta in choices[0].delta.content
800
+ for await (const chunk of stream) {
801
+ const delta = chunk.choices[0]?.delta?.content;
802
+ if (delta) process.stdout.write(delta);
803
+ }
804
+ ```
805
+
806
+ #### Parameters
807
+
808
+ - `model` (string, required): AI model to use (e.g., 'anthropic/claude-3.5-haiku', 'openai/gpt-4', etc.)
809
+ - `messages` (array): Conversation messages with role ('system', 'user', 'assistant') and content
810
+ - `temperature` (number): Sampling temperature (0-2)
811
+ - `maxTokens` (number): Maximum tokens to generate
812
+ - `topP` (number): Top-p sampling parameter (0-1)
813
+ - `stream` (boolean): Enable streaming mode
814
+ - `thinking` (boolean): Enable chain-of-thought reasoning (supported models)
815
+ - `webSearch`, `fileParser`, `tools`, `toolChoice`, `parallelToolCalls`: Optional plugin/tool-calling options — see the SDK source for their shapes
816
+
817
+ ### `ai.images.generate()`
818
+
819
+ Generate images using AI models.
820
+
821
+ ```javascript
822
+ // Text-to-image
823
+ const image = await insforge.ai.images.generate({
824
+ model: "google/gemini-3-pro-image-preview",
825
+ prompt: "A serene landscape with mountains at sunset",
826
+ });
827
+ // Returns an OpenAI-like image object directly (not a { data, error } envelope):
828
+ // image.data[i].b64_json - base64-encoded image (no `data:` URI prefix)
829
+ // image.data[i].content - text output, for text-capable image models
830
+ // image.usage - token usage (when reported by the model)
831
+ const base64Png = image.data[0].b64_json;
832
+
833
+ // Image-to-image — pass source images as URLs or base64 data URIs
834
+ const edited = await insforge.ai.images.generate({
835
+ model: "google/gemini-3-pro-image-preview",
836
+ prompt: "Turn this into a watercolor painting",
837
+ images: [{ url: "https://example.com/input.jpg" }],
838
+ });
839
+ ```
840
+
841
+ #### Parameters
842
+
843
+ - `model` (string, required): Image generation model (e.g., 'google/gemini-3-pro-image-preview', 'openai/dall-e-3')
844
+ - `prompt` (string, required): Text description of the image to generate
845
+ - `images` (array): Optional source images for image-to-image, each `{ url: string }` (HTTPS URL or `data:` base64 URI)
846
+
847
+ > The SDK normalizes generated images to base64 and exposes them as `image.data[i].b64_json`.
848
+
849
+ ### `ai.embeddings.create()`
850
+
851
+ Create embeddings for one or more text inputs.
852
+
853
+ ```javascript
854
+ const embeddings = await insforge.ai.embeddings.create({
855
+ model: "openai/text-embedding-3-small",
856
+ input: "Hello world", // or string[] for batch input
857
+ });
858
+ // Returns an OpenAI-like embeddings object directly (not a { data, error } envelope):
859
+ // embeddings.data[i].embedding - the vector (number[]) for input i
860
+ // embeddings.usage - token usage information
861
+ // embeddings.model - the model used
862
+ console.log(embeddings.data[0].embedding);
863
+ ```
864
+
865
+ #### Parameters
866
+
867
+ - `model` (string, required): Embedding model (e.g., 'openai/text-embedding-3-small')
868
+ - `input` (string | string[], required): Text(s) to embed
869
+ - `dimensions` (number): Output dimensions, if supported by the model
870
+ - `encoding_format` ('float' | 'base64'): Encoding of the returned vectors
871
+
872
+ ### Complete AI Example
873
+
874
+ ```javascript
875
+ import { createClient } from "@insforge/sdk";
876
+
877
+ const insforge = createClient({
878
+ baseUrl: "http://localhost:7130",
879
+ });
880
+
881
+ // Chat completion
882
+ const chat = await insforge.ai.chat.completions.create({
883
+ model: "anthropic/claude-3.5-haiku",
884
+ messages: [{ role: "user", content: "What is the capital of France?" }],
885
+ });
886
+ console.log(chat.choices[0].message.content); // "The capital of France is Paris."
887
+
888
+ // Streaming chat
889
+ const stream = await insforge.ai.chat.completions.create({
890
+ model: "anthropic/claude-3.5-haiku",
891
+ messages: [{ role: "user", content: "Write a haiku about coding" }],
892
+ stream: true,
893
+ });
894
+
895
+ let fullResponse = "";
896
+ for await (const chunk of stream) {
897
+ const delta = chunk.choices[0]?.delta?.content;
898
+ if (delta) {
899
+ fullResponse += delta;
900
+ process.stdout.write(delta);
901
+ }
902
+ }
903
+
904
+ // Image generation
905
+ const image = await insforge.ai.images.generate({
906
+ model: "google/gemini-3-pro-image-preview",
907
+ prompt: "A futuristic city with flying cars",
908
+ });
909
+ const base64Png = image.data[0].b64_json; // base64-encoded image
910
+
911
+ // Embeddings
912
+ const embeddings = await insforge.ai.embeddings.create({
913
+ model: "openai/text-embedding-3-small",
914
+ input: "Vectorize this sentence",
915
+ });
916
+ console.log(embeddings.data[0].embedding); // number[]
917
+ ```
918
+
919
+ ## Types (from @insforge/shared-schemas)
920
+
921
+ ```typescript
922
+ import type {
923
+ UserSchema,
924
+ CreateUserRequest,
925
+ CreateSessionRequest,
926
+ GetCurrentSessionResponse,
927
+ StorageFileSchema,
928
+ StorageBucketSchema,
929
+ ListObjectsResponseSchema,
930
+ PublicOAuthProvider,
931
+ GetPublicEmailAuthConfigResponse,
932
+ } from "@insforge/shared-schemas";
933
+
934
+ // Database response type
935
+ interface DatabaseResponse<T> {
936
+ data: T | null;
937
+ error: InsForgeError | null;
938
+ count?: number;
939
+ }
940
+
941
+ // Storage response type
942
+ interface StorageResponse<T> {
943
+ data: T | null;
944
+ error: InsForgeError | null;
945
+ }
946
+ ```