@buildbase/sdk 0.0.40 → 0.0.42

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 CHANGED
@@ -10,6 +10,8 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
10
10
  - [Installation](#installation)
11
11
  - [Quick Start](#quick-start)
12
12
  - [Authentication](#-authentication)
13
+ - [Redirect Preservation](#redirect-preservation)
14
+ - [Affiliate / Referral Tracking](#affiliate--referral-tracking)
13
15
  - [Role-Based Access Control](#-role-based-access-control)
14
16
  - [Feature Flags](#️-feature-flags)
15
17
  - [Subscription Gates](#-subscription-gates)
@@ -35,7 +37,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
35
37
 
36
38
  ## 🚀 Features
37
39
 
38
- - **🔐 Authentication System** - Complete auth flow with sign-in/sign-out
40
+ - **🔐 Authentication System** - Complete auth flow with sign-in/sign-out and redirect preservation
39
41
  - **🏢 Workspace Management** - Multi-workspace support with switching capabilities
40
42
  - **👥 Role-Based Access Control** - User roles and workspace-specific permissions
41
43
  - **🎯 Feature Flags** - Workspace-level and user-level feature toggles
@@ -45,6 +47,7 @@ Also works server-side (Next.js API routes, Express, Hono) — see [Server-Side
45
47
  - **📬 Notifications** - Email + push notification system with per-event channel control, workspace preferences, and server-side `notification.send()` API
46
48
  - **💺 Seat-Based Pricing** - Per-seat billing with included seats, billable seat tracking, and seat limit enforcement
47
49
  - **💱 Multi-Currency** - Per-currency pricing variants with workspace billing currency lock
50
+ - **🤝 Affiliate Tracking** - Pass referral data to Stripe checkout via `stripeOptions` (Rewardful, Endorsely, FirstPromoter, etc.)
48
51
  - **📊 Quota Usage Tracking** - Record and monitor metered usage (API calls, storage, etc.) with real-time status
49
52
  - **📈 Usage Dashboard** - Built-in workspace settings page showing quota consumption, overage billing breakdowns, and billing period info
50
53
  - **👤 User Management** - User attributes and feature flags management
@@ -67,17 +70,17 @@ npm install @buildbase/sdk react@^19.0.0 react-dom@^19.0.0
67
70
 
68
71
  ```tsx
69
72
  // app/layout.tsx (or your root layout)
70
- import '@buildbase/sdk/css'
73
+ import '@buildbase/sdk/css';
71
74
  ```
72
75
 
73
76
  ### 2. Create Provider
74
77
 
75
78
  ```tsx
76
79
  // components/provider.tsx
77
- 'use client'
80
+ 'use client';
78
81
 
79
- import { SaaSOSProvider } from '@buildbase/sdk/react'
80
- import { ApiVersion } from '@buildbase/sdk'
82
+ import { SaaSOSProvider } from '@buildbase/sdk/react';
83
+ import { ApiVersion } from '@buildbase/sdk';
81
84
 
82
85
  export default function SaaSProvider({ children }: { children: React.ReactNode }) {
83
86
  return (
@@ -91,40 +94,40 @@ export default function SaaSProvider({ children }: { children: React.ReactNode }
91
94
  callbacks: {
92
95
  // Called on page refresh to restore session from httpOnly cookie
93
96
  getSession: async () => {
94
- const res = await fetch('/api/auth/session')
95
- const data = await res.json()
96
- return data.sessionId ?? null
97
+ const res = await fetch('/api/auth/session');
98
+ const data = await res.json();
99
+ return data.sessionId ?? null;
97
100
  },
98
101
 
99
102
  // Called after OAuth redirect to exchange code for sessionId
100
- handleAuthentication: async (code) => {
103
+ handleAuthentication: async code => {
101
104
  const res = await fetch('/api/auth/verify', {
102
105
  method: 'POST',
103
106
  headers: { 'Content-Type': 'application/json' },
104
107
  body: JSON.stringify({ code }),
105
- })
106
- const data = await res.json()
107
- return { sessionId: data.sessionId }
108
+ });
109
+ const data = await res.json();
110
+ return { sessionId: data.sessionId };
108
111
  },
109
112
 
110
113
  // Called on sign out to clear the httpOnly cookie
111
114
  onSignOut: async () => {
112
- await fetch('/api/auth/signout', { method: 'POST' })
113
- window.location.reload()
115
+ await fetch('/api/auth/signout', { method: 'POST' });
116
+ window.location.reload();
114
117
  },
115
118
 
116
119
  handleEvent: (eventType, data) => {
117
- console.log('SDK Event:', eventType, data)
120
+ console.log('SDK Event:', eventType, data);
118
121
  },
119
122
  onWorkspaceChange: async ({ workspace, user, role }) => {
120
- console.log('Switching to:', workspace.name, 'as', role)
123
+ console.log('Switching to:', workspace.name, 'as', role);
121
124
  },
122
125
  },
123
126
  }}
124
127
  >
125
128
  {children}
126
129
  </SaaSOSProvider>
127
- )
130
+ );
128
131
  }
129
132
  ```
130
133
 
@@ -138,8 +141,8 @@ The SDK uses the same session pattern as next-auth: the session token lives in a
138
141
 
139
142
  ```tsx
140
143
  // app/layout.tsx
141
- import SaaSProvider from '@/components/provider'
142
- import '@buildbase/sdk/css'
144
+ import SaaSProvider from '@/components/provider';
145
+ import '@buildbase/sdk/css';
143
146
 
144
147
  export default function RootLayout({ children }) {
145
148
  return (
@@ -148,7 +151,7 @@ export default function RootLayout({ children }) {
148
151
  <SaaSProvider>{children}</SaaSProvider>
149
152
  </body>
150
153
  </html>
151
- )
154
+ );
152
155
  }
153
156
  ```
154
157
 
@@ -212,7 +215,7 @@ function AuthExample() {
212
215
  {!isAuthenticated ? (
213
216
  <div>
214
217
  <h1>Welcome! Please sign in</h1>
215
- <button onClick={signIn} disabled={status === 'loading'}>
218
+ <button onClick={() => signIn()} disabled={status === 'loading'}>
216
219
  {status === 'loading' ? 'Signing in...' : 'Sign In'}
217
220
  </button>
218
221
  </div>
@@ -239,7 +242,7 @@ const {
239
242
  isLoading, // Boolean: true when checking authentication status
240
243
  isRedirecting, // Boolean: true when redirecting for OAuth
241
244
  status, // AuthStatus: 'loading' | 'redirecting' | 'authenticating' | 'authenticated' | 'unauthenticated' (use AuthStatus enum for type-safe checks)
242
- signIn, // Function: initiates sign-in flow
245
+ signIn, // Function: initiates sign-in flow. Accepts optional returnUrl to redirect back after login.
243
246
  signOut, // Function: signs out the user
244
247
  openWorkspaceSettings, // Function: opens workspace settings dialog to a specific section
245
248
  } = useSaaSAuth();
@@ -250,13 +253,13 @@ const {
250
253
  Open the workspace settings dialog to a specific section:
251
254
 
252
255
  ```tsx
253
- openWorkspaceSettings('profile'); // Account profile
254
- openWorkspaceSettings('general'); // Workspace name, icon
255
- openWorkspaceSettings('users'); // Workspace members
256
+ openWorkspaceSettings('profile'); // Account profile
257
+ openWorkspaceSettings('general'); // Workspace name, icon
258
+ openWorkspaceSettings('users'); // Workspace members
256
259
  openWorkspaceSettings('subscription'); // Plan & Billing
257
- openWorkspaceSettings('usage'); // Quota usage dashboard
258
- openWorkspaceSettings('features'); // Feature toggles
259
- openWorkspaceSettings('danger'); // Delete workspace (owner only)
260
+ openWorkspaceSettings('usage'); // Quota usage dashboard
261
+ openWorkspaceSettings('features'); // Feature toggles
262
+ openWorkspaceSettings('danger'); // Delete workspace (owner only)
260
263
  ```
261
264
 
262
265
  ### Authentication Components
@@ -281,6 +284,52 @@ function App() {
281
284
  }
282
285
  ```
283
286
 
287
+ ### Redirect Preservation
288
+
289
+ The SDK automatically preserves the URL when `signIn()` is called. After login, the user is redirected back to the page they were on.
290
+
291
+ ```tsx
292
+ // Automatic — just call signIn(), the current URL is saved
293
+ signIn();
294
+
295
+ // Custom — pass a specific URL to redirect to after login
296
+ signIn('https://app.com/dashboard?bb=action:selectPlan,plan:abc');
297
+ ```
298
+
299
+ This works across the full OAuth round-trip via localStorage (10-minute TTL, validated with `validateRedirectUrl()`).
300
+
301
+ For advanced use cases, the low-level helpers are also exported:
302
+
303
+ ```tsx
304
+ import { saveAuthIntent, consumeAuthIntent, clearAuthIntent } from '@buildbase/sdk';
305
+ ```
306
+
307
+ ### Affiliate / Referral Tracking
308
+
309
+ Pass affiliate data to Stripe checkout sessions via `stripeOptions` on the checkout request:
310
+
311
+ ```tsx
312
+ createCheckoutSession({
313
+ planVersionId: '...',
314
+ stripeOptions: {
315
+ // Rewardful, FirstPromoter, PartnerStack — read client_reference_id
316
+ clientReferenceId: Rewardful.referral,
317
+
318
+ // Endorsely — reads subscription metadata
319
+ subscriptionMetadata: { endorsely_referral: window.endorsely_referral },
320
+
321
+ // Custom tracking on the checkout session
322
+ metadata: { campaign: 'summer-sale' },
323
+ },
324
+ });
325
+ ```
326
+
327
+ | Field | Stripe mapping | Use case |
328
+ | ---------------------- | ----------------------------- | -------------------------------------- |
329
+ | `clientReferenceId` | `client_reference_id` | Rewardful, FirstPromoter, etc. |
330
+ | `metadata` | `metadata` (checkout session) | Custom tracking, Endorsely |
331
+ | `subscriptionMetadata` | `subscription_data.metadata` | Data that persists on the subscription |
332
+
284
333
  ## 👥 Role-Based Access Control
285
334
 
286
335
  ### Role Components
@@ -374,7 +423,11 @@ Control UI visibility based on the current workspace’s subscription. Subscript
374
423
  ### Subscription Gate Components
375
424
 
376
425
  ```tsx
377
- import { WhenSubscription, WhenNoSubscription, WhenSubscriptionToPlans } from '@buildbase/sdk/react';
426
+ import {
427
+ WhenSubscription,
428
+ WhenNoSubscription,
429
+ WhenSubscriptionToPlans,
430
+ } from '@buildbase/sdk/react';
378
431
 
379
432
  function BillingExample() {
380
433
  return (
@@ -489,10 +542,10 @@ function TrialExample() {
489
542
  }
490
543
  ```
491
544
 
492
- | Component | Renders when |
493
- | ----------------- | -------------------------------------------------------- |
494
- | `WhenTrialing` | Subscription status is `trialing` |
495
- | `WhenNotTrialing` | Subscription status is NOT `trialing` |
545
+ | Component | Renders when |
546
+ | ----------------- | --------------------------------------------------------------- |
547
+ | `WhenTrialing` | Subscription status is `trialing` |
548
+ | `WhenNotTrialing` | Subscription status is NOT `trialing` |
496
549
  | `WhenTrialEnding` | Trialing AND trial ends within `daysThreshold` days (default 3) |
497
550
 
498
551
  All trial gates support `loadingComponent` and `fallbackComponent` props.
@@ -518,13 +571,13 @@ function TrialInfo() {
518
571
  }
519
572
  ```
520
573
 
521
- | Property | Type | Description |
522
- | --------------- | -------------- | ------------------------------------------------ |
523
- | `isTrialing` | `boolean` | Whether subscription is in trial |
524
- | `daysRemaining` | `number` | Days left in trial (0 if not trialing or expired) |
525
- | `trialEndsAt` | `Date \| null` | Trial end date |
526
- | `trialStartedAt`| `Date \| null` | Trial start date |
527
- | `isTrialEnding` | `boolean` | True when 3 or fewer days remaining |
574
+ | Property | Type | Description |
575
+ | ---------------- | -------------- | ------------------------------------------------- |
576
+ | `isTrialing` | `boolean` | Whether subscription is in trial |
577
+ | `daysRemaining` | `number` | Days left in trial (0 if not trialing or expired) |
578
+ | `trialEndsAt` | `Date \| null` | Trial end date |
579
+ | `trialStartedAt` | `Date \| null` | Trial start date |
580
+ | `isTrialEnding` | `boolean` | True when 3 or fewer days remaining |
528
581
 
529
582
  ## 🔔 Push Notifications
530
583
 
@@ -533,7 +586,7 @@ Browser push notifications — built into the SDK. Users can enable/disable from
533
586
  **Only setup required:** Create `public/push-sw.js` in your app:
534
587
 
535
588
  ```js
536
- self.addEventListener('push', function(event) {
589
+ self.addEventListener('push', function (event) {
537
590
  if (!event.data) return;
538
591
  try {
539
592
  var payload = event.data.json();
@@ -552,17 +605,23 @@ self.addEventListener('push', function(event) {
552
605
  data: { url: payload.url, ...(payload.data || {}) },
553
606
  };
554
607
  event.waitUntil(self.registration.showNotification(payload.title || 'Notification', options));
555
- } catch (e) { console.error('[PushSW]', e); }
608
+ } catch (e) {
609
+ console.error('[PushSW]', e);
610
+ }
556
611
  });
557
612
 
558
- self.addEventListener('notificationclick', function(event) {
613
+ self.addEventListener('notificationclick', function (event) {
559
614
  event.notification.close();
560
615
  var url = event.notification.data && event.notification.data.url;
561
616
  if (url) {
562
- event.waitUntil(clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(list) {
563
- for (var i = 0; i < list.length; i++) { if (list[i].url === url && 'focus' in list[i]) return list[i].focus(); }
564
- if (clients.openWindow) return clients.openWindow(url);
565
- }));
617
+ event.waitUntil(
618
+ clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function (list) {
619
+ for (var i = 0; i < list.length; i++) {
620
+ if (list[i].url === url && 'focus' in list[i]) return list[i].focus();
621
+ }
622
+ if (clients.openWindow) return clients.openWindow(url);
623
+ })
624
+ );
566
625
  }
567
626
  });
568
627
  ```
@@ -580,16 +639,16 @@ Send email and push notifications to workspace members. The system has three lay
580
639
  ### Sending Notifications (Server-Side)
581
640
 
582
641
  ```ts
583
- import { notification } from '@/lib/buildbase'
642
+ import { notification } from '@/lib/buildbase';
584
643
 
585
644
  // Notify a specific user
586
645
  await notification.send(workspaceId, 'comment_added', userId, {
587
- title: 'New Comment', // Push title (falls back to event name)
646
+ title: 'New Comment', // Push title (falls back to event name)
588
647
  message: 'Alice commented on your project', // Push body + email {{message}}
589
648
  icon: 'https://example.com/comment-icon.png', // Custom push icon (falls back to org icon)
590
- image: 'https://example.com/screenshot.jpg', // Large image in push notification body
649
+ image: 'https://example.com/screenshot.jpg', // Large image in push notification body
591
650
  url: 'https://app.example.com/projects/123#comments', // Opens on push click + {{url}} in email
592
- })
651
+ });
593
652
 
594
653
  // Notify all workspace members (omit userId)
595
654
  await notification.send(workspaceId, 'new_release', undefined, {
@@ -597,7 +656,7 @@ await notification.send(workspaceId, 'new_release', undefined, {
597
656
  message: 'Version 2.0 is now available with dark mode and API v2!',
598
657
  image: 'https://example.com/release-banner.jpg',
599
658
  url: 'https://app.example.com/changelog',
600
- })
659
+ });
601
660
  ```
602
661
 
603
662
  ### Ad-hoc Notifications
@@ -612,7 +671,7 @@ await notification.send(workspaceId, 'deployment_success', userId, {
612
671
  icon: 'https://example.com/deploy-icon.png',
613
672
  url: '/deployments/latest',
614
673
  channels: { push: true },
615
- })
674
+ });
616
675
  ```
617
676
 
618
677
  > **Note:** Ad-hoc events support push only. Email requires a registered event with a linked email template.
@@ -630,20 +689,20 @@ await notification.send(workspaceId, 'new_message', userId, {
630
689
  { action: 'reply', title: 'Reply', icon: 'https://example.com/reply.png' },
631
690
  { action: 'dismiss', title: 'Dismiss' },
632
691
  ],
633
- tag: 'chat-alice', // Replaces previous "chat-alice" notification instead of stacking
634
- renotify: true, // Still vibrate/sound when replacing
692
+ tag: 'chat-alice', // Replaces previous "chat-alice" notification instead of stacking
693
+ renotify: true, // Still vibrate/sound when replacing
635
694
  channels: { push: true },
636
- })
695
+ });
637
696
 
638
697
  // Critical alert — stays visible until user interacts
639
698
  await notification.send(workspaceId, 'payment_failed', userId, {
640
699
  title: 'Payment Failed',
641
700
  message: 'Your subscription will be suspended in 3 days',
642
701
  badge: 'https://example.com/alert-badge.png',
643
- requireInteraction: true, // No auto-dismiss
644
- urgency: 'high', // Prioritized delivery on mobile
702
+ requireInteraction: true, // No auto-dismiss
703
+ urgency: 'high', // Prioritized delivery on mobile
645
704
  channels: { push: true },
646
- })
705
+ });
647
706
 
648
707
  // Silent notification — no sound or vibration
649
708
  await notification.send(workspaceId, 'sync_complete', userId, {
@@ -652,19 +711,19 @@ await notification.send(workspaceId, 'sync_complete', userId, {
652
711
  silent: true,
653
712
  urgency: 'low',
654
713
  channels: { push: true },
655
- })
714
+ });
656
715
  ```
657
716
 
658
- | Option | Type | Description |
659
- |--------|------|-------------|
660
- | `badge` | `string` | Small monochrome icon for status bar (Android/ChromeOS) |
661
- | `tag` | `string` | Replaces existing notification with same tag instead of stacking |
662
- | `actions` | `Array<{action, title, icon?}>` | Up to 2 action buttons on the notification |
663
- | `silent` | `boolean` | No sound or vibration |
664
- | `requireInteraction` | `boolean` | Stays visible until user interacts |
665
- | `renotify` | `boolean` | Sound/vibrate again when replacing via `tag` |
666
- | `timestamp` | `number` | Custom timestamp (ms since epoch) shown on notification |
667
- | `dir` | `'ltr' \| 'rtl' \| 'auto'` | Text direction for title/body |
717
+ | Option | Type | Description |
718
+ | -------------------- | ------------------------------- | ---------------------------------------------------------------- |
719
+ | `badge` | `string` | Small monochrome icon for status bar (Android/ChromeOS) |
720
+ | `tag` | `string` | Replaces existing notification with same tag instead of stacking |
721
+ | `actions` | `Array<{action, title, icon?}>` | Up to 2 action buttons on the notification |
722
+ | `silent` | `boolean` | No sound or vibration |
723
+ | `requireInteraction` | `boolean` | Stays visible until user interacts |
724
+ | `renotify` | `boolean` | Sound/vibrate again when replacing via `tag` |
725
+ | `timestamp` | `number` | Custom timestamp (ms since epoch) shown on notification |
726
+ | `dir` | `'ltr' \| 'rtl' \| 'auto'` | Text direction for title/body |
668
727
 
669
728
  ### Delivery Options
670
729
 
@@ -675,26 +734,26 @@ Control how and when the push service delivers the notification:
675
734
  await notification.send(workspaceId, 'daily_digest', undefined, {
676
735
  title: 'Your Daily Digest',
677
736
  message: '12 new updates in your workspace',
678
- scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
737
+ scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
679
738
  channels: { push: true },
680
- })
739
+ });
681
740
 
682
741
  // Short-lived notification — discard if not delivered in 1 hour
683
742
  await notification.send(workspaceId, 'flash_sale', undefined, {
684
743
  title: 'Flash Sale — 50% off!',
685
744
  message: 'Ends in 1 hour',
686
745
  image: 'https://example.com/sale-banner.jpg',
687
- ttl: 3600, // Expires after 1 hour (seconds)
688
- urgency: 'high', // Deliver ASAP
746
+ ttl: 3600, // Expires after 1 hour (seconds)
747
+ urgency: 'high', // Deliver ASAP
689
748
  channels: { push: true },
690
- })
749
+ });
691
750
  ```
692
751
 
693
- | Option | Type | Description |
694
- |--------|------|-------------|
695
- | `scheduledAt` | `string` | ISO 8601 date. Delays delivery until the specified time |
696
- | `ttl` | `number` | Time-to-live in seconds. Push service discards if not delivered in time. Default: 86400 (24h) |
697
- | `urgency` | `'very-low' \| 'low' \| 'normal' \| 'high'` | Delivery priority hint. Affects battery usage on mobile |
752
+ | Option | Type | Description |
753
+ | ------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------- |
754
+ | `scheduledAt` | `string` | ISO 8601 date. Delays delivery until the specified time |
755
+ | `ttl` | `number` | Time-to-live in seconds. Push service discards if not delivered in time. Default: 86400 (24h) |
756
+ | `urgency` | `'very-low' \| 'low' \| 'normal' \| 'high'` | Delivery priority hint. Affects battery usage on mobile |
698
757
 
699
758
  ### Channel Control
700
759
 
@@ -705,19 +764,19 @@ By default, both email and push are sent (based on event config). Override per-s
705
764
  await notification.send(workspaceId, 'typing_indicator', userId, {
706
765
  message: 'Alice is typing...',
707
766
  channels: { push: true },
708
- })
767
+ });
709
768
 
710
769
  // Only email — digest or report, no push
711
770
  await notification.send(workspaceId, 'weekly_report', undefined, {
712
771
  message: 'Your weekly activity report is ready',
713
772
  channels: { email: true },
714
- })
773
+ });
715
774
 
716
775
  // Both channels explicitly
717
776
  await notification.send(workspaceId, 'comment_added', userId, {
718
777
  message: 'New comment on your project',
719
778
  channels: { email: true, push: true },
720
- })
779
+ });
721
780
  ```
722
781
 
723
782
  > **Note:** Even with `channels` override, the 4-layer gate still applies. If the admin disabled push globally, `channels: { push: true }` won't send push.
@@ -728,21 +787,21 @@ Both email and push support merge tags with `{{tag}}` syntax:
728
787
 
729
788
  ```ts
730
789
  await notification.send(workspaceId, 'export_ready', userId, {
731
- title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
790
+ title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
732
791
  message: 'Hi {{name}}, your export is ready', // → "Hi Alice, your export is ready"
733
792
  downloadUrl: 'https://example.com/exports/123',
734
793
  fileName: 'report.csv',
735
- })
794
+ });
736
795
  ```
737
796
 
738
- | Tag | Resolves to | Available in |
739
- |-----|-------------|--------------|
740
- | `{{name}}` | Recipient's name | Email + Push |
741
- | `{{email}}` | Recipient's email | Email + Push |
742
- | `{{workspaceName}}` | Workspace name | Email + Push |
743
- | `{{message}}` | The `message` field | Email template |
744
- | `{{url}}` | The `url` field | Email template + Push click target |
745
- | `{{anyKey}}` | Value from `data` object | Email + Push |
797
+ | Tag | Resolves to | Available in |
798
+ | ------------------- | ------------------------ | ---------------------------------- |
799
+ | `{{name}}` | Recipient's name | Email + Push |
800
+ | `{{email}}` | Recipient's email | Email + Push |
801
+ | `{{workspaceName}}` | Workspace name | Email + Push |
802
+ | `{{message}}` | The `message` field | Email template |
803
+ | `{{url}}` | The `url` field | Email template + Push click target |
804
+ | `{{anyKey}}` | Value from `data` object | Email + Push |
746
805
 
747
806
  ### Response
748
807
 
@@ -780,7 +839,7 @@ An email template is auto-created for each custom event. Edit it in **Email Temp
780
839
  The workspace settings panel shows notification preferences automatically — **only for events where the developer enabled "User Control"**. System notifications are never shown to end users.
781
840
 
782
841
  ```tsx
783
- import { SaaSOSProvider } from '@buildbase/sdk/react'
842
+ import { SaaSOSProvider } from '@buildbase/sdk/react';
784
843
 
785
844
  // The Notifications tab in workspace settings shows:
786
845
  // - Browser push toggle (subscribe/unsubscribe)
@@ -790,34 +849,34 @@ import { SaaSOSProvider } from '@buildbase/sdk/react'
790
849
  ### Types
791
850
 
792
851
  ```ts
793
- import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk'
852
+ import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk';
794
853
 
795
854
  interface NotificationData {
796
- title?: string; // Push title (falls back to event name)
797
- message?: string; // Push body + email {{message}}
798
- icon?: string; // Custom push icon URL (falls back to org icon)
799
- image?: string; // Large image in push notification body
800
- badge?: string; // Small monochrome status bar icon
801
- url?: string; // Opens on push click + {{url}} in email
802
- tag?: string; // Replace instead of stack notifications
803
- actions?: Array<{action, title, icon?}>; // Action buttons (max 2)
804
- silent?: boolean; // No sound/vibration
805
- requireInteraction?: boolean; // No auto-dismiss
806
- renotify?: boolean; // Re-alert on tag replace
807
- timestamp?: number; // Custom timestamp (ms)
808
- dir?: 'ltr' | 'rtl' | 'auto'; // Text direction
809
- ttl?: number; // Time-to-live (seconds)
810
- urgency?: 'very-low'|'low'|'normal'|'high'; // Delivery priority
811
- scheduledAt?: string; // ISO 8601 delayed delivery
855
+ title?: string; // Push title (falls back to event name)
856
+ message?: string; // Push body + email {{message}}
857
+ icon?: string; // Custom push icon URL (falls back to org icon)
858
+ image?: string; // Large image in push notification body
859
+ badge?: string; // Small monochrome status bar icon
860
+ url?: string; // Opens on push click + {{url}} in email
861
+ tag?: string; // Replace instead of stack notifications
862
+ actions?: Array<{ action; title; icon? }>; // Action buttons (max 2)
863
+ silent?: boolean; // No sound/vibration
864
+ requireInteraction?: boolean; // No auto-dismiss
865
+ renotify?: boolean; // Re-alert on tag replace
866
+ timestamp?: number; // Custom timestamp (ms)
867
+ dir?: 'ltr' | 'rtl' | 'auto'; // Text direction
868
+ ttl?: number; // Time-to-live (seconds)
869
+ urgency?: 'very-low' | 'low' | 'normal' | 'high'; // Delivery priority
870
+ scheduledAt?: string; // ISO 8601 delayed delivery
812
871
  channels?: { email?: boolean; push?: boolean }; // Override which channels to use
813
- [key: string]: any; // Custom merge tags for email + push
872
+ [key: string]: any; // Custom merge tags for email + push
814
873
  }
815
874
 
816
875
  interface NotificationResult {
817
876
  sent: boolean;
818
877
  channels: { email: boolean; push: boolean };
819
878
  notifiedCount?: number;
820
- reason?: string; // Only when sent=false
879
+ reason?: string; // Only when sent=false
821
880
  }
822
881
 
823
882
  interface NotificationEvent {
@@ -836,23 +895,21 @@ The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and numb
836
895
  ### Setup
837
896
 
838
897
  ```tsx
839
- <SaaSOSProvider locale="hi">
840
- {/* All SDK UI renders in Hindi */}
841
- </SaaSOSProvider>
898
+ <SaaSOSProvider locale="hi">{/* All SDK UI renders in Hindi */}</SaaSOSProvider>
842
899
  ```
843
900
 
844
901
  ### Supported Locales
845
902
 
846
- | Code | Language | Numerals | Direction |
847
- |---|---|---|---|
848
- | `en` | English | 1,234.56 | LTR |
849
- | `es` | Spanish | 1.234,56 | LTR |
850
- | `fr` | French | 1 234,56 | LTR |
851
- | `de` | German | 1.234,56 | LTR |
852
- | `ja` | Japanese | 1,234.56 | LTR |
853
- | `zh` | Chinese | 1,234.56 | LTR |
854
- | `hi` | Hindi | Devanagari (e.g. 1,234) | LTR |
855
- | `ar` | Arabic | Arabic-Indic (e.g. 1,234) | RTL |
903
+ | Code | Language | Numerals | Direction |
904
+ | ---- | -------- | ------------------------- | --------- |
905
+ | `en` | English | 1,234.56 | LTR |
906
+ | `es` | Spanish | 1.234,56 | LTR |
907
+ | `fr` | French | 1 234,56 | LTR |
908
+ | `de` | German | 1.234,56 | LTR |
909
+ | `ja` | Japanese | 1,234.56 | LTR |
910
+ | `zh` | Chinese | 1,234.56 | LTR |
911
+ | `hi` | Hindi | Devanagari (e.g. 1,234) | LTR |
912
+ | `ar` | Arabic | Arabic-Indic (e.g. 1,234) | RTL |
856
913
 
857
914
  ### useTranslation Hook
858
915
 
@@ -864,10 +921,10 @@ function MyComponent() {
864
921
 
865
922
  return (
866
923
  <div dir={dir}>
867
- <p>{t('subscription.currentPlan')}</p> {/* Type-safe key lookup */}
868
- <p>{t('users.memberCount', { count: 5 })}</p> {/* ICU plural: "5 members" */}
869
- <p>{fmtNum(1234)}</p> {/* Locale-aware: "1,234" or "1,234" */}
870
- <p>{fmtCents(1999, 'usd')}</p> {/* "$19.99" or "19.99 US$" */}
924
+ <p>{t('subscription.currentPlan')}</p> {/* Type-safe key lookup */}
925
+ <p>{t('users.memberCount', { count: 5 })}</p> {/* ICU plural: "5 members" */}
926
+ <p>{fmtNum(1234)}</p> {/* Locale-aware: "1,234" or "1,234" */}
927
+ <p>{fmtCents(1999, 'usd')}</p> {/* "$19.99" or "19.99 US$" */}
871
928
  </div>
872
929
  );
873
930
  }
@@ -909,13 +966,13 @@ For full SaaS platforms (Slack, GitHub, Discord):
909
966
 
910
967
  Platform mode supports granular overrides from the admin dashboard:
911
968
 
912
- | Setting | Options | Default |
913
- |---|---|---|
914
- | Can Create Workspace | Everyone / Owner Only / Disabled | Everyone |
915
- | Can Invite Members | Everyone / Admin Only / Disabled | Everyone |
916
- | Show Workspace Switcher | On / Off | On |
917
- | Max Workspaces Per User | 0 (unlimited) or a number | 0 |
918
- | Auto-Create First Workspace | On / Off | On |
969
+ | Setting | Options | Default |
970
+ | --------------------------- | -------------------------------- | -------- |
971
+ | Can Create Workspace | Everyone / Owner Only / Disabled | Everyone |
972
+ | Can Invite Members | Everyone / Admin Only / Disabled | Everyone |
973
+ | Show Workspace Switcher | On / Off | On |
974
+ | Max Workspaces Per User | 0 (unlimited) or a number | 0 |
975
+ | Auto-Create First Workspace | On / Off | On |
919
976
 
920
977
  These let you achieve team-like, managed, or enterprise-like behavior without a separate mode.
921
978
 
@@ -984,6 +1041,8 @@ function WorkspaceManager() {
984
1041
  updateFeature, // Toggle workspace feature
985
1042
  getProfile, // Get current user profile
986
1043
  updateUserProfile, // Update user profile
1044
+ updateWorkspaceSettings, // Update workspace settings
1045
+ updateWorkspacePermissions, // Update workspace permissions
987
1046
  } = useSaaSWorkspaces();
988
1047
 
989
1048
  // Example: Create a workspace
@@ -1036,15 +1095,22 @@ import { PricingPage } from '@buildbase/sdk/react';
1036
1095
 
1037
1096
  function PublicPricingPage() {
1038
1097
  return (
1039
- <PricingPage slug="main-pricing">
1040
- {({ loading, error, items, plans, refetch }) => {
1098
+ <PricingPage slug="main-pricing" redirectBaseUrl="https://app.com/dashboard">
1099
+ {({ loading, error, items, plans, selectPlan, refetch }) => {
1041
1100
  if (loading) return <Loading />;
1042
1101
  if (error) return <Error message={error} />;
1043
1102
 
1044
1103
  return (
1045
1104
  <div>
1046
1105
  {plans.map(plan => (
1047
- <PlanCard key={plan._id} plan={plan} items={items} />
1106
+ <div key={plan._id}>
1107
+ <PlanCard plan={plan} items={items} />
1108
+ <button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
1109
+ {plan.trial?.enabled
1110
+ ? `Start ${plan.trial.durationDays}-Day Trial`
1111
+ : 'Select Plan'}
1112
+ </button>
1113
+ </div>
1048
1114
  ))}
1049
1115
  </div>
1050
1116
  );
@@ -1054,12 +1120,22 @@ function PublicPricingPage() {
1054
1120
  }
1055
1121
  ```
1056
1122
 
1057
- | Prop | Type | Description |
1058
- | ----------------- | ------------------------------ | ----------------------------------------------------------------- |
1059
- | `slug` | `string` | Plan group slug (e.g. 'main-pricing', 'enterprise') |
1060
- | `children` | `(details) => ReactNode` | Render prop receiving `{ loading, error, items, plans, refetch }` |
1061
- | `loadingFallback` | `ReactNode` | Custom loading UI (defaults to skeleton) |
1062
- | `errorFallback` | `(error: string) => ReactNode` | Custom error UI |
1123
+ `selectPlan()` handles everything automatically:
1124
+
1125
+ - **Authenticated** → opens the "Choose Your Plan" dialog
1126
+ - **Not authenticated** → saves a redirect URL, triggers sign-in, and after login the user lands on the dashboard with the plan picker dialog open
1127
+
1128
+ | Prop | Type | Description |
1129
+ | ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------- |
1130
+ | `slug` | `string` | Plan group slug (e.g. 'main-pricing', 'enterprise') |
1131
+ | `children` | `(details) => ReactNode` | Render prop receiving plan details (see below) |
1132
+ | `redirectBaseUrl` | `string` | Base URL for post-login redirects (e.g. `"https://app.com/dashboard"`). Enables `selectPlan()` for unauthenticated users. |
1133
+ | `loadingFallback` | `ReactNode` | Custom loading UI (defaults to skeleton) |
1134
+ | `errorFallback` | `(error: string) => ReactNode` | Custom error UI |
1135
+
1136
+ **Render prop details**: `{ loading, error, items, plans, notes, refetch, selectPlan }`
1137
+
1138
+ - `selectPlan(planVersionId, interval, currency)` — One-call plan selection (handles auth + dialog automatically)
1063
1139
 
1064
1140
  **Response shape**: `items` = subscription item definitions (features, limits, quotas with category); `plans` = plan versions with `pricing`, `quotas`, `features`, `limits`.
1065
1141
 
@@ -1071,41 +1147,41 @@ Plans support **pricing variants** (multi-currency). Use these utilities for dis
1071
1147
 
1072
1148
  ### Currency utilities
1073
1149
 
1074
- | Export | Purpose |
1075
- |--------|--------|
1076
- | `CURRENCY_DISPLAY` | Map of currency code → symbol (e.g. `usd` → `$`) |
1077
- | `CURRENCY_FLAG` | Map of currency code → flag emoji |
1078
- | `PLAN_CURRENCY_CODES` | Allowed billing currency codes (for dropdowns/validation) |
1079
- | `PLAN_CURRENCY_OPTIONS` | Options array for plan currency selects |
1080
- | `getCurrencySymbol(currency)` | Symbol for a Stripe currency code |
1081
- | `getCurrencyFlag(currency)` | Flag emoji for a currency code |
1082
- | `formatCents(cents, currency)` | Format cents as localized price string |
1083
- | `formatOverageRate(cents, currency)` | Format overage rate for display |
1084
- | `formatOverageRateWithLabel(...)` | Overage rate with optional unit label |
1085
- | `formatQuotaIncludedOverage(...)` | "X included, then $Y / unit" style text |
1086
- | `getQuotaUnitLabelFromName(name)` | Human-readable unit label from quota name |
1150
+ | Export | Purpose |
1151
+ | ------------------------------------ | --------------------------------------------------------- |
1152
+ | `CURRENCY_DISPLAY` | Map of currency code → symbol (e.g. `usd` → `$`) |
1153
+ | `CURRENCY_FLAG` | Map of currency code → flag emoji |
1154
+ | `PLAN_CURRENCY_CODES` | Allowed billing currency codes (for dropdowns/validation) |
1155
+ | `PLAN_CURRENCY_OPTIONS` | Options array for plan currency selects |
1156
+ | `getCurrencySymbol(currency)` | Symbol for a Stripe currency code |
1157
+ | `getCurrencyFlag(currency)` | Flag emoji for a currency code |
1158
+ | `formatCents(cents, currency)` | Format cents as localized price string |
1159
+ | `formatOverageRate(cents, currency)` | Format overage rate for display |
1160
+ | `formatOverageRateWithLabel(...)` | Overage rate with optional unit label |
1161
+ | `formatQuotaIncludedOverage(...)` | "X included, then $Y / unit" style text |
1162
+ | `getQuotaUnitLabelFromName(name)` | Human-readable unit label from quota name |
1087
1163
 
1088
1164
  ### Pricing variant utilities
1089
1165
 
1090
- | Export | Purpose |
1091
- |--------|--------|
1092
- | `getPricingVariant(planVersion, currency)` | Get variant for a currency, or `null` |
1093
- | `getBasePriceCents(planVersion, currency, interval)` | Base price in cents for currency/interval |
1094
- | `getStripePriceIdForInterval(planVersion, currency, interval)` | Stripe price ID for checkout |
1095
- | `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)` | Overage cents for a quota |
1096
- | `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant |
1097
- | `getAvailableCurrenciesFromPlans(plans)` | Unique currency codes across plan versions |
1098
- | `getDisplayCurrency(planVersion, currency)` | Display currency (variant exists ? currency : plan.currency) |
1099
- | `getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId)` | Resolve price ID to interval + currency |
1166
+ | Export | Purpose |
1167
+ | ------------------------------------------------------------------------ | ------------------------------------------------------------ |
1168
+ | `getPricingVariant(planVersion, currency)` | Get variant for a currency, or `null` |
1169
+ | `getBasePriceCents(planVersion, currency, interval)` | Base price in cents for currency/interval |
1170
+ | `getStripePriceIdForInterval(planVersion, currency, interval)` | Stripe price ID for checkout |
1171
+ | `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)` | Overage cents for a quota |
1172
+ | `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant |
1173
+ | `getAvailableCurrenciesFromPlans(plans)` | Unique currency codes across plan versions |
1174
+ | `getDisplayCurrency(planVersion, currency)` | Display currency (variant exists ? currency : plan.currency) |
1175
+ | `getBillingIntervalAndCurrencyFromPriceId(planVersions, priceId)` | Resolve price ID to interval + currency |
1100
1176
 
1101
1177
  Types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage`.
1102
1178
 
1103
1179
  ### Quota utilities
1104
1180
 
1105
- | Export | Purpose |
1106
- |--------|--------|
1181
+ | Export | Purpose |
1182
+ | -------------------------------------------------- | ------------------------------------------------------------------- |
1107
1183
  | `getQuotaDisplayValue(quotaByInterval, interval?)` | Normalize `IQuotaByInterval` to `{ included, overage?, unitSize? }` |
1108
- | `formatQuotaWithPrice(value, unitName, options?)` | Format as "X included, then $Y.YY / unit" |
1184
+ | `formatQuotaWithPrice(value, unitName, options?)` | Format as "X included, then $Y.YY / unit" |
1109
1185
 
1110
1186
  Types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan/subscription types use `IQuotaByInterval` and `IQuotaIntervalValue` for per-interval quotas and overages.
1111
1187
 
@@ -1137,12 +1213,12 @@ Track and monitor metered usage for subscription quotas (e.g., API calls, emails
1137
1213
 
1138
1214
  ### When to use which?
1139
1215
 
1140
- | Scenario | Where to record | Why |
1141
- |----------|----------------|-----|
1216
+ | Scenario | Where to record | Why |
1217
+ | ------------------------------- | ---------------------- | -------------------------------------------- |
1142
1218
  | User clicks "Send Email" button | Client-side (SDK hook) | User-initiated, immediate UI feedback needed |
1143
- | API request hits your backend | Server-side (REST API) | Backend controls the resource, more secure |
1144
- | Background job processes data | Server-side (REST API) | No browser context available |
1145
- | File upload completes | Either | Depends on where validation happens |
1219
+ | API request hits your backend | Server-side (REST API) | Backend controls the resource, more secure |
1220
+ | Background job processes data | Server-side (REST API) | No browser context available |
1221
+ | File upload completes | Either | Depends on where validation happens |
1146
1222
 
1147
1223
  As a general rule: **record usage where the resource is consumed**. If your backend processes the work, record from the backend. If it's a client-side action, record from the client.
1148
1224
 
@@ -1166,8 +1242,8 @@ function SendEmailButton() {
1166
1242
  const result = await recordUsage({
1167
1243
  quotaSlug: 'emails',
1168
1244
  quantity: 1,
1169
- source: 'web-app', // optional: track where usage came from
1170
- idempotencyKey: 'email-abc', // optional: prevent duplicate recordings
1245
+ source: 'web-app', // optional: track where usage came from
1246
+ idempotencyKey: 'email-abc', // optional: prevent duplicate recordings
1171
1247
  });
1172
1248
  console.log(`Used: ${result.consumed}/${result.included}, Available: ${result.available}`);
1173
1249
  if (result.overage > 0) {
@@ -1178,7 +1254,11 @@ function SendEmailButton() {
1178
1254
  }
1179
1255
  };
1180
1256
 
1181
- return <button onClick={handleSend} disabled={loading}>Send Email</button>;
1257
+ return (
1258
+ <button onClick={handleSend} disabled={loading}>
1259
+ Send Email
1260
+ </button>
1261
+ );
1182
1262
  }
1183
1263
  ```
1184
1264
 
@@ -1198,7 +1278,9 @@ function QuotaStatusBar({ quotaSlug }: { quotaSlug: string }) {
1198
1278
 
1199
1279
  return (
1200
1280
  <div>
1201
- <p>{quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)</p>
1281
+ <p>
1282
+ {quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)
1283
+ </p>
1202
1284
  <p>Available: {status.available}</p>
1203
1285
  {status.hasOverage && <p>Overage: {status.overage} units</p>}
1204
1286
  <button onClick={refetch}>Refresh</button>
@@ -1239,12 +1321,10 @@ import { useUsageLogs, useSaaSWorkspaces } from '@buildbase/sdk/react';
1239
1321
 
1240
1322
  function UsageLogsTable() {
1241
1323
  const { currentWorkspace } = useSaaSWorkspaces();
1242
- const {
1243
- logs, totalDocs, totalPages, page, hasNextPage, loading, refetch,
1244
- } = useUsageLogs(
1324
+ const { logs, totalDocs, totalPages, page, hasNextPage, loading, refetch } = useUsageLogs(
1245
1325
  currentWorkspace?._id,
1246
- 'api_calls', // optional: filter by quota slug
1247
- { limit: 20, page: 1 } // optional: pagination and filters
1326
+ 'api_calls', // optional: filter by quota slug
1327
+ { limit: 20, page: 1 } // optional: pagination and filters
1248
1328
  );
1249
1329
 
1250
1330
  if (loading) return <Spinner />;
@@ -1253,7 +1333,12 @@ function UsageLogsTable() {
1253
1333
  <div>
1254
1334
  <table>
1255
1335
  <thead>
1256
- <tr><th>Quota</th><th>Quantity</th><th>Source</th><th>Date</th></tr>
1336
+ <tr>
1337
+ <th>Quota</th>
1338
+ <th>Quantity</th>
1339
+ <th>Source</th>
1340
+ <th>Date</th>
1341
+ </tr>
1257
1342
  </thead>
1258
1343
  <tbody>
1259
1344
  {logs.map(log => (
@@ -1266,7 +1351,9 @@ function UsageLogsTable() {
1266
1351
  ))}
1267
1352
  </tbody>
1268
1353
  </table>
1269
- <p>Page {page} of {totalPages} ({totalDocs} total)</p>
1354
+ <p>
1355
+ Page {page} of {totalPages} ({totalDocs} total)
1356
+ </p>
1270
1357
  </div>
1271
1358
  );
1272
1359
  }
@@ -1274,23 +1361,23 @@ function UsageLogsTable() {
1274
1361
 
1275
1362
  **`useUsageLogs` parameters:**
1276
1363
 
1277
- | Parameter | Type | Required | Description |
1278
- |-----------|------|----------|-------------|
1279
- | `workspaceId` | `string \| null \| undefined` | Yes | Workspace ID (null/undefined disables fetching) |
1280
- | `quotaSlug` | `string` | No | Filter logs by quota slug |
1281
- | `options.from` | `string` | No | ISO date string — filter logs from this date |
1282
- | `options.to` | `string` | No | ISO date string — filter logs until this date |
1283
- | `options.source` | `string` | No | Filter logs by source |
1284
- | `options.page` | `number` | No | Page number (default: 1) |
1285
- | `options.limit` | `number` | No | Results per page (default: 20) |
1364
+ | Parameter | Type | Required | Description |
1365
+ | ---------------- | ----------------------------- | -------- | ----------------------------------------------- |
1366
+ | `workspaceId` | `string \| null \| undefined` | Yes | Workspace ID (null/undefined disables fetching) |
1367
+ | `quotaSlug` | `string` | No | Filter logs by quota slug |
1368
+ | `options.from` | `string` | No | ISO date string — filter logs from this date |
1369
+ | `options.to` | `string` | No | ISO date string — filter logs until this date |
1370
+ | `options.source` | `string` | No | Filter logs by source |
1371
+ | `options.page` | `number` | No | Page number (default: 1) |
1372
+ | `options.limit` | `number` | No | Results per page (default: 20) |
1286
1373
 
1287
1374
  #### Client-Side Hooks Summary
1288
1375
 
1289
- | Hook | Purpose |
1290
- |------|---------|
1291
- | `useRecordUsage(workspaceId)` | Record quota consumption (mutation) |
1292
- | `useQuotaUsageStatus(workspaceId, quotaSlug)` | Get single quota status (auto-fetches) |
1293
- | `useAllQuotaUsage(workspaceId)` | Get all quotas status (auto-fetches) |
1376
+ | Hook | Purpose |
1377
+ | ------------------------------------------------- | ------------------------------------------ |
1378
+ | `useRecordUsage(workspaceId)` | Record quota consumption (mutation) |
1379
+ | `useQuotaUsageStatus(workspaceId, quotaSlug)` | Get single quota status (auto-fetches) |
1380
+ | `useAllQuotaUsage(workspaceId)` | Get all quotas status (auto-fetches) |
1294
1381
  | `useUsageLogs(workspaceId, quotaSlug?, options?)` | Get paginated usage history (auto-fetches) |
1295
1382
 
1296
1383
  ---
@@ -1337,8 +1424,8 @@ async function recordUsage(workspaceId: string, quotaSlug: string, quantity: num
1337
1424
  body: JSON.stringify({
1338
1425
  quotaSlug,
1339
1426
  quantity,
1340
- source: 'backend', // optional: helps distinguish from client-side usage
1341
- metadata: {}, // optional: attach custom data
1427
+ source: 'backend', // optional: helps distinguish from client-side usage
1428
+ metadata: {}, // optional: attach custom data
1342
1429
  idempotencyKey: undefined, // optional: prevent duplicate recordings
1343
1430
  }),
1344
1431
  });
@@ -1376,10 +1463,9 @@ async function getQuotaStatus(workspaceId: string, quotaSlug: string) {
1376
1463
 
1377
1464
  // Get status for ALL quotas
1378
1465
  async function getAllQuotaStatus(workspaceId: string) {
1379
- const response = await fetch(
1380
- `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`,
1381
- { headers: { 'x-session-id': SESSION_ID } }
1382
- );
1466
+ const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`, {
1467
+ headers: { 'x-session-id': SESSION_ID },
1468
+ });
1383
1469
  return response.json();
1384
1470
  // Returns: { quotas: { [slug]: { consumed, included, available, overage, hasOverage } } }
1385
1471
  }
@@ -1416,26 +1502,23 @@ await bb.usage.recordBatch(workspaceId, {
1416
1502
  { quotaSlug: 'images', quantity: 500, source: 'batch-export-job' },
1417
1503
  { quotaSlug: 'videos', quantity: 10, source: 'batch-export-job' },
1418
1504
  { quotaSlug: 'images', quantity: 200, metadata: { jobId: 'abc123' } },
1419
- ]
1505
+ ],
1420
1506
  });
1421
1507
  // Returns: { success, total: 3, succeeded: 3, failed: 0, results: [...] }
1422
1508
  ```
1423
1509
 
1424
1510
  ```ts
1425
1511
  // Using REST API directly
1426
- const response = await fetch(
1427
- `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/batch`,
1428
- {
1429
- method: 'POST',
1430
- headers: { 'x-session-id': SESSION_ID, 'Content-Type': 'application/json' },
1431
- body: JSON.stringify({
1432
- items: [
1433
- { quotaSlug: 'images', quantity: 500, source: 'batch-export' },
1434
- { quotaSlug: 'videos', quantity: 10 },
1435
- ]
1436
- }),
1437
- }
1438
- );
1512
+ const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/batch`, {
1513
+ method: 'POST',
1514
+ headers: { 'x-session-id': SESSION_ID, 'Content-Type': 'application/json' },
1515
+ body: JSON.stringify({
1516
+ items: [
1517
+ { quotaSlug: 'images', quantity: 500, source: 'batch-export' },
1518
+ { quotaSlug: 'videos', quantity: 10 },
1519
+ ],
1520
+ }),
1521
+ });
1439
1522
  ```
1440
1523
 
1441
1524
  - Max **100 items** per request
@@ -1445,35 +1528,35 @@ const response = await fetch(
1445
1528
 
1446
1529
  #### Server-Side API Reference
1447
1530
 
1448
- | Endpoint | Method | Description |
1449
- |----------|--------|-------------|
1450
- | `/api/v1/public/token/exchange` | POST | Exchange API token for session ID |
1451
- | `/api/v1/public/workspaces/:id/subscription/usage` | POST | Record quota usage |
1452
- | `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET | Get single quota status |
1453
- | `/api/v1/public/workspaces/:id/subscription/usage/all` | GET | Get all quotas status |
1454
- | `/api/v1/public/workspaces/:id/subscription/usage/logs` | GET | Get paginated usage logs |
1531
+ | Endpoint | Method | Description |
1532
+ | --------------------------------------------------------------------- | ------ | --------------------------------- |
1533
+ | `/api/v1/public/token/exchange` | POST | Exchange API token for session ID |
1534
+ | `/api/v1/public/workspaces/:id/subscription/usage` | POST | Record quota usage |
1535
+ | `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET | Get single quota status |
1536
+ | `/api/v1/public/workspaces/:id/subscription/usage/all` | GET | Get all quotas status |
1537
+ | `/api/v1/public/workspaces/:id/subscription/usage/logs` | GET | Get paginated usage logs |
1455
1538
 
1456
1539
  All endpoints (except `/token/exchange`) require the `x-session-id` header.
1457
1540
 
1458
1541
  **Record usage request body:**
1459
1542
 
1460
- | Field | Type | Required | Description |
1461
- |-------|------|----------|-------------|
1462
- | `quotaSlug` | `string` | Yes | Quota identifier (e.g. `'api_calls'`, `'emails'`, `'storage'`) |
1463
- | `quantity` | `number` | Yes | Units to consume (minimum 1) |
1464
- | `metadata` | `object` | No | Custom metadata to attach to the usage record |
1465
- | `source` | `string` | No | Source identifier (e.g. `'backend'`, `'worker'`, `'cron'`) |
1466
- | `idempotencyKey` | `string` | No | Unique key for deduplication |
1543
+ | Field | Type | Required | Description |
1544
+ | ---------------- | -------- | -------- | -------------------------------------------------------------- |
1545
+ | `quotaSlug` | `string` | Yes | Quota identifier (e.g. `'api_calls'`, `'emails'`, `'storage'`) |
1546
+ | `quantity` | `number` | Yes | Units to consume (minimum 1) |
1547
+ | `metadata` | `object` | No | Custom metadata to attach to the usage record |
1548
+ | `source` | `string` | No | Source identifier (e.g. `'backend'`, `'worker'`, `'cron'`) |
1549
+ | `idempotencyKey` | `string` | No | Unique key for deduplication |
1467
1550
 
1468
1551
  **Record usage response:**
1469
1552
 
1470
- | Field | Type | Description |
1471
- |-------|------|-------------|
1472
- | `used` | `number` | Quantity recorded in this request |
1473
- | `consumed` | `number` | Total usage in the current billing period |
1474
- | `included` | `number` | Total units included in the plan |
1475
- | `available` | `number` | Remaining units before overage |
1476
- | `overage` | `number` | Units used beyond the included amount |
1553
+ | Field | Type | Description |
1554
+ | ------------- | --------- | -------------------------------------------- |
1555
+ | `used` | `number` | Quantity recorded in this request |
1556
+ | `consumed` | `number` | Total usage in the current billing period |
1557
+ | `included` | `number` | Total units included in the plan |
1558
+ | `available` | `number` | Remaining units before overage |
1559
+ | `overage` | `number` | Units used beyond the included amount |
1477
1560
  | `billedAsync` | `boolean` | Whether overage billing was queued to Stripe |
1478
1561
 
1479
1562
  ## 🚦 Quota Gates
@@ -1542,12 +1625,12 @@ All quota gate components support optional `loadingComponent` and `fallbackCompo
1542
1625
 
1543
1626
  ### Quota Gate Components Reference
1544
1627
 
1545
- | Component | Renders children when | Props |
1546
- |-----------|----------------------|-------|
1547
- | `WhenQuotaAvailable` | Quota has remaining units (`available > 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1548
- | `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1549
- | `WhenQuotaOverage` | Usage exceeds included amount (`hasOverage`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1550
- | `WhenQuotaThreshold` | Usage percentage >= threshold | `slug`, `threshold` (0-100), `children`, `loadingComponent?`, `fallbackComponent?` |
1628
+ | Component | Renders children when | Props |
1629
+ | -------------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
1630
+ | `WhenQuotaAvailable` | Quota has remaining units (`available > 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1631
+ | `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1632
+ | `WhenQuotaOverage` | Usage exceeds included amount (`hasOverage`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1633
+ | `WhenQuotaThreshold` | Usage percentage >= threshold | `slug`, `threshold` (0-100), `children`, `loadingComponent?`, `fallbackComponent?` |
1551
1634
 
1552
1635
  All gates must be used inside `QuotaUsageContextProvider` (included in `SaaSOSProvider`). By default they return `null` while loading or when the condition is not met.
1553
1636
 
@@ -1567,7 +1650,9 @@ function QuotaDebug() {
1567
1650
  return (
1568
1651
  <div>
1569
1652
  {Object.entries(quotas).map(([slug, usage]) => (
1570
- <p key={slug}>{slug}: {usage.consumed}/{usage.included}</p>
1653
+ <p key={slug}>
1654
+ {slug}: {usage.consumed}/{usage.included}
1655
+ </p>
1571
1656
  ))}
1572
1657
  <button onClick={() => refetch()}>Refresh</button>
1573
1658
  </div>
@@ -1575,13 +1660,14 @@ function QuotaDebug() {
1575
1660
  }
1576
1661
  ```
1577
1662
 
1578
- | Property | Type | Description |
1579
- |----------|------|-------------|
1580
- | `quotas` | `Record<string, IQuotaUsageStatus> \| null` | Current quota usage data keyed by slug |
1581
- | `loading` | `boolean` | True while quota data is being fetched |
1582
- | `refetch` | `() => Promise<void>` | Manually refetch all quota usage |
1663
+ | Property | Type | Description |
1664
+ | --------- | ------------------------------------------- | -------------------------------------- |
1665
+ | `quotas` | `Record<string, IQuotaUsageStatus> \| null` | Current quota usage data keyed by slug |
1666
+ | `loading` | `boolean` | True while quota data is being fetched |
1667
+ | `refetch` | `() => Promise<void>` | Manually refetch all quota usage |
1583
1668
 
1584
1669
  **When quota usage refetches:**
1670
+
1585
1671
  - When the current workspace changes (automatic).
1586
1672
  - When usage is recorded via `useRecordUsage` — refetch is triggered automatically.
1587
1673
  - When you call `refetch()` manually.
@@ -1598,7 +1684,6 @@ function SignupPage() {
1598
1684
  <BetaForm
1599
1685
  onSuccess={() => console.log('Form submitted!')}
1600
1686
  onError={error => console.error(error)}
1601
- language="en" // Optional: 'en' | 'es' | 'fr' | 'de' | 'zh' | 'ja' | 'ko'
1602
1687
  showSuccessMessage={true}
1603
1688
  hideLogo={false}
1604
1689
  hideTitles={false}
@@ -1685,25 +1770,25 @@ All SDK API clients extend a shared base class and are exported from the package
1685
1770
  | `BaseApi` | Abstract base (URL, auth, `fetchJson`/`fetchResponse`) – extend for custom APIs |
1686
1771
  | `IBaseApiConfig` | Config type: `serverUrl`, `version`, optional `orgId` |
1687
1772
  | `UserApi` | User attributes and features |
1688
- | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
1773
+ | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
1689
1774
  | `SettingsApi` | Organization settings |
1690
1775
 
1691
1776
  ### Components
1692
1777
 
1693
- | Component | Purpose |
1694
- | --- | --- |
1695
- | `SubscriptionContextProvider` | Provides subscription data to children (included in SaaSOSProvider) |
1696
- | `WhenSubscription`, `WhenNoSubscription`, `WhenSubscriptionToPlans` | Subscription gate components |
1697
- | `WhenTrialing`, `WhenNotTrialing`, `WhenTrialEnding` | Trial gate components |
1698
- | `WhenQuotaAvailable`, `WhenQuotaExhausted`, `WhenQuotaOverage` | Quota gate components |
1778
+ | Component | Purpose |
1779
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------- |
1780
+ | `SubscriptionContextProvider` | Provides subscription data to children (included in SaaSOSProvider) |
1781
+ | `WhenSubscription`, `WhenNoSubscription`, `WhenSubscriptionToPlans` | Subscription gate components |
1782
+ | `WhenTrialing`, `WhenNotTrialing`, `WhenTrialEnding` | Trial gate components |
1783
+ | `WhenQuotaAvailable`, `WhenQuotaExhausted`, `WhenQuotaOverage` | Quota gate components |
1699
1784
 
1700
1785
  ### Currency, pricing variant & quota utilities
1701
1786
 
1702
- | Category | Exports |
1703
- | -------- | ------- |
1704
- | **Currency** | `CURRENCY_DISPLAY`, `CURRENCY_FLAG`, `PLAN_CURRENCY_CODES`, `PLAN_CURRENCY_OPTIONS`, `getCurrencySymbol`, `getCurrencyFlag`, `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaIncludedOverage`, `getQuotaUnitLabelFromName` |
1787
+ | Category | Exports |
1788
+ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1789
+ | **Currency** | `CURRENCY_DISPLAY`, `CURRENCY_FLAG`, `PLAN_CURRENCY_CODES`, `PLAN_CURRENCY_OPTIONS`, `getCurrencySymbol`, `getCurrencyFlag`, `formatCents`, `formatOverageRate`, `formatOverageRateWithLabel`, `formatQuotaIncludedOverage`, `getQuotaUnitLabelFromName` |
1705
1790
  | **Pricing variants** | `getPricingVariant`, `getBasePriceCents`, `getStripePriceIdForInterval`, `getQuotaOverageCents`, `getQuotaDisplayWithVariant`, `getAvailableCurrenciesFromPlans`, `getDisplayCurrency`, `getBillingIntervalAndCurrencyFromPriceId`; types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage` |
1706
- | **Quota** | `getQuotaDisplayValue`, `formatQuotaWithPrice`; types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan types use `IQuotaByInterval`, `IQuotaIntervalValue` for per-interval quotas. |
1791
+ | **Quota** | `getQuotaDisplayValue`, `formatQuotaWithPrice`; types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan types use `IQuotaByInterval`, `IQuotaIntervalValue` for per-interval quotas. |
1707
1792
 
1708
1793
  Get OS config from `useSaaSOs()` and instantiate API classes when you need low-level access; otherwise prefer the high-level hooks (`useSaaSWorkspaces`, `useUserAttributes`, `useSaaSSettings`, etc.):
1709
1794
 
@@ -1724,21 +1809,21 @@ const workspaceApi = new WorkspaceApi({
1724
1809
 
1725
1810
  Prefer these SDK hooks for state and operations instead of `useAppSelector`:
1726
1811
 
1727
- | Hook | Purpose |
1728
- | -------------------------- | ------------------------------------------------------------------------------------------------------- |
1729
- | `useSaaSAuth()` | Auth state (user, session, status), signIn, signOut, openWorkspaceSettings |
1730
- | `useSaaSWorkspaces()` | Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions |
1731
- | `useSaaSOs()` | OS config (serverUrl, version, orgId, auth, settings) when you need the full config object |
1732
- | `useSaaSSettings()` | Organization settings and getSettings (prefer this when you only need settings) |
1733
- | `useUserAttributes()` | User attributes and update/refresh |
1734
- | `useUserFeatures()` | User feature flags |
1735
- | `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
1736
- | `useTrialStatus()` | Trial state: `isTrialing`, `daysRemaining`, `trialEndsAt`, `isTrialEnding` |
1737
- | `usePushNotifications()` | Push notification state and actions: `isSubscribed`, `subscribe()`, `unsubscribe()` |
1812
+ | Hook | Purpose |
1813
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1814
+ | `useSaaSAuth()` | Auth state (user, session, status), signIn(returnUrl?), signOut, openWorkspaceSettings |
1815
+ | `useSaaSWorkspaces()` | Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions |
1816
+ | `useSaaSOs()` | OS config (serverUrl, version, orgId, auth, settings) when you need the full config object |
1817
+ | `useSaaSSettings()` | Organization settings and getSettings (prefer this when you only need settings) |
1818
+ | `useUserAttributes()` | User attributes and update/refresh |
1819
+ | `useUserFeatures()` | User feature flags |
1820
+ | `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
1821
+ | `useTrialStatus()` | Trial state: `isTrialing`, `daysRemaining`, `trialEndsAt`, `isTrialEnding` |
1822
+ | `usePushNotifications()` | Push notification state and actions: `isSubscribed`, `subscribe()`, `unsubscribe()` |
1738
1823
  | Subscription hooks | `usePublicPlans`, `useSubscription`, `useSubscriptionManagement`, `usePlanGroup`, `usePlanGroupVersions`, `usePublicPlanGroupVersion`, `useCreateCheckoutSession`, `useUpdateSubscription`, `useCancelSubscription`, `useResumeSubscription`, `useInvoices`, `useInvoice` |
1739
- | `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider |
1740
- | Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs` |
1741
- | Invalidation helpers | `invalidateSubscription()`, `invalidateQuotaUsage()` — trigger context refetch after server-side mutations |
1824
+ | `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider |
1825
+ | Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs` |
1826
+ | Invalidation helpers | `invalidateSubscription()`, `invalidateQuotaUsage()` — trigger context refetch after server-side mutations |
1742
1827
 
1743
1828
  Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.
1744
1829
 
@@ -1796,6 +1881,7 @@ interface OnWorkspaceChangeParams {
1796
1881
  ```
1797
1882
 
1798
1883
  **Session flow** (same pattern as next-auth):
1884
+
1799
1885
  - Session token is stored in an **httpOnly cookie** (set by your server, not readable by JS)
1800
1886
  - On page refresh, the SDK calls `getSession()` once to restore the session
1801
1887
  - Session data (user info) lives **in-memory only** (React context) — no localStorage
@@ -1808,19 +1894,18 @@ interface OnWorkspaceChangeParams {
1808
1894
 
1809
1895
  ### BetaForm Props
1810
1896
 
1811
- | Prop | Type | Default | Description |
1812
- | ------------------------ | ------------------------------------------------------ | -------------------------------- | --------------------------------------- |
1813
- | `onSuccess` | `() => void` | - | Callback when form submits successfully |
1814
- | `onError` | `(error: string) => void` | - | Callback when form submission fails |
1815
- | `className` | `string` | `'w-full'` | CSS class for form container |
1816
- | `fieldClassName` | `string` | `'flex flex-col gap-1.5 w-full'` | CSS class for form fields |
1817
- | `language` | `'en' \| 'es' \| 'fr' \| 'de' \| 'zh' \| 'ja' \| 'ko'` | Auto-detect | Form language |
1818
- | `customTexts` | `Partial<FormText>` | `{}` | Custom text overrides |
1819
- | `autoFocus` | `boolean` | `true` | Auto-focus name field |
1820
- | `showSuccessMessage` | `boolean` | `true` | Show success message after submit |
1821
- | `successMessageDuration` | `number` | - | Duration to show success message (ms) |
1822
- | `hideLogo` | `boolean` | `false` | Hide logo |
1823
- | `hideTitles` | `boolean` | `false` | Hide titles |
1897
+ | Prop | Type | Default | Description |
1898
+ | -------------------- | ------------------------- | -------------------------------- | --------------------------------------- |
1899
+ | `onSuccess` | `() => void` | - | Callback when form submits successfully |
1900
+ | `onError` | `(error: string) => void` | - | Callback when form submission fails |
1901
+ | `className` | `string` | `'w-full'` | CSS class for form container |
1902
+ | `fieldClassName` | `string` | `'flex flex-col gap-1.5 w-full'` | CSS class for form fields |
1903
+ | `autoFocus` | `boolean` | `true` | Auto-focus name field |
1904
+ | `showSuccessMessage` | `boolean` | `true` | Show success message after submit |
1905
+ | `hideLogo` | `boolean` | `false` | Hide logo |
1906
+ | `hideTitles` | `boolean` | `false` | Hide titles |
1907
+
1908
+ > **Note:** The form language is inherited from the `SaaSOSProvider` locale setting. Supported locales: en, es, fr, de, ja, zh, hi, ar.
1824
1909
 
1825
1910
  ## 🎯 Common Patterns
1826
1911
 
@@ -1918,7 +2003,11 @@ function Dashboard() {
1918
2003
  ### Pattern 4b: Subscription-Gated UI
1919
2004
 
1920
2005
  ```tsx
1921
- import { WhenSubscription, WhenNoSubscription, WhenSubscriptionToPlans } from '@buildbase/sdk/react';
2006
+ import {
2007
+ WhenSubscription,
2008
+ WhenNoSubscription,
2009
+ WhenSubscriptionToPlans,
2010
+ } from '@buildbase/sdk/react';
1922
2011
 
1923
2012
  function BillingPage() {
1924
2013
  return (
@@ -1958,7 +2047,9 @@ function FeatureWithQuota() {
1958
2047
  </WhenQuotaAvailable>
1959
2048
 
1960
2049
  <WhenQuotaExhausted slug="api_calls">
1961
- <p>No API calls remaining. <a href="/billing">Upgrade your plan</a></p>
2050
+ <p>
2051
+ No API calls remaining. <a href="/billing">Upgrade your plan</a>
2052
+ </p>
1962
2053
  </WhenQuotaExhausted>
1963
2054
  </div>
1964
2055
  );
@@ -2246,7 +2337,7 @@ if (isFeatureEnabled('premium')) {
2246
2337
  ```tsx
2247
2338
  // ✅ Good
2248
2339
  const { signIn, status } = useSaaSAuth();
2249
- <button onClick={signIn} disabled={status === 'loading'}>
2340
+ <button onClick={() => signIn()} disabled={status === 'loading'}>
2250
2341
  {status === 'loading' ? 'Signing in...' : 'Sign In'}
2251
2342
  </button>;
2252
2343
  ```
@@ -2320,31 +2411,45 @@ Configure once, use everywhere. Same pattern as Auth.js.
2320
2411
 
2321
2412
  ```ts
2322
2413
  // lib/buildbase.ts
2323
- import BuildBase from '@buildbase/sdk'
2324
- import { cookies } from 'next/headers'
2325
-
2326
- export const { auth, workspace, subscription, usage, plans, invoices, users, features, settings, notification, permissions, withSession, client } = BuildBase({
2414
+ import BuildBase from '@buildbase/sdk';
2415
+ import { cookies } from 'next/headers';
2416
+
2417
+ export const {
2418
+ auth,
2419
+ workspace,
2420
+ subscription,
2421
+ usage,
2422
+ plans,
2423
+ invoices,
2424
+ users,
2425
+ features,
2426
+ settings,
2427
+ notification,
2428
+ permissions,
2429
+ withSession,
2430
+ client,
2431
+ } = BuildBase({
2327
2432
  serverUrl: process.env.BUILDBASE_URL!,
2328
2433
  orgId: process.env.BUILDBASE_ORG_ID!,
2329
2434
  getSessionId: async () => {
2330
- const c = await cookies()
2331
- return c.get('bb-session-id')?.value ?? null
2435
+ const c = await cookies();
2436
+ return c.get('bb-session-id')?.value ?? null;
2332
2437
  },
2333
- })
2438
+ });
2334
2439
  ```
2335
2440
 
2336
2441
  Use in API routes — session is resolved automatically:
2337
2442
 
2338
2443
  ```ts
2339
2444
  // app/api/workspace/route.ts
2340
- import { auth, workspace, subscription } from '@/lib/buildbase'
2445
+ import { auth, workspace, subscription } from '@/lib/buildbase';
2341
2446
 
2342
2447
  export async function GET() {
2343
- const session = await auth()
2344
- if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
2448
+ const session = await auth();
2449
+ if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
2345
2450
 
2346
- const workspaces = await workspace.list()
2347
- return Response.json({ workspaces })
2451
+ const workspaces = await workspace.list();
2452
+ return Response.json({ workspaces });
2348
2453
  }
2349
2454
  ```
2350
2455
 
@@ -2354,31 +2459,31 @@ No `getSessionId` callback — use `withSession()` per-request instead.
2354
2459
 
2355
2460
  ```ts
2356
2461
  // buildbase.ts
2357
- import BuildBase from '@buildbase/sdk'
2462
+ import BuildBase from '@buildbase/sdk';
2358
2463
 
2359
2464
  const bb = BuildBase({
2360
2465
  serverUrl: process.env.BUILDBASE_URL!,
2361
2466
  orgId: process.env.BUILDBASE_ORG_ID!,
2362
- })
2467
+ });
2363
2468
 
2364
- export const { withSession, plans } = bb
2469
+ export const { withSession, plans } = bb;
2365
2470
  ```
2366
2471
 
2367
2472
  ```ts
2368
2473
  // routes
2369
2474
  app.get('/workspaces', async (req, res) => {
2370
- const { workspace } = withSession(req.headers['x-session-id'])
2371
- res.json(await workspace.list())
2372
- })
2475
+ const { workspace } = withSession(req.headers['x-session-id']);
2476
+ res.json(await workspace.list());
2477
+ });
2373
2478
 
2374
2479
  app.post('/usage', async (req, res) => {
2375
- const { usage } = withSession(req.headers['x-session-id'])
2480
+ const { usage } = withSession(req.headers['x-session-id']);
2376
2481
  const result = await usage.record(req.body.workspaceId, {
2377
2482
  quotaSlug: 'api-calls',
2378
2483
  quantity: 1,
2379
- })
2380
- res.json(result)
2381
- })
2484
+ });
2485
+ res.json(result);
2486
+ });
2382
2487
  ```
2383
2488
 
2384
2489
  ### Background Jobs / Webhooks
@@ -2386,35 +2491,35 @@ app.post('/usage', async (req, res) => {
2386
2491
  For service accounts (no user session), use `withSession()` with a service token:
2387
2492
 
2388
2493
  ```ts
2389
- import { withSession } from '@/lib/buildbase'
2494
+ import { withSession } from '@/lib/buildbase';
2390
2495
 
2391
- const bb = withSession(process.env.SERVICE_SESSION_ID!)
2496
+ const bb = withSession(process.env.SERVICE_SESSION_ID!);
2392
2497
 
2393
2498
  // Record usage from a webhook
2394
2499
  await bb.usage.record(workspaceId, {
2395
2500
  quotaSlug: 'uploads',
2396
2501
  quantity: 1,
2397
2502
  source: 'webhook:file.processed',
2398
- })
2503
+ });
2399
2504
 
2400
2505
  // Check subscription
2401
- const sub = await bb.subscription.get(workspaceId)
2506
+ const sub = await bb.subscription.get(workspaceId);
2402
2507
  ```
2403
2508
 
2404
2509
  ### Server-Side Actions Reference
2405
2510
 
2406
- | Module | Methods |
2407
- |--------|---------|
2408
- | `workspace` | `list`, `get`, `create`, `update`, `delete` |
2409
- | `users` | `list`, `invite`, `remove`, `updateRole`, `getProfile`, `updateProfile` |
2410
- | `subscription` | `get`, `checkout`, `update`, `cancel`, `resume`, `getBillingPortalUrl` |
2411
- | `plans` | `getGroup`, `getVersions`, `getPublic`, `getVersion` |
2412
- | `invoices` | `list`, `get` |
2413
- | `usage` | `record`, `recordBatch`, `getQuota`, `getAll`, `getLogs` |
2414
- | `settings` | `get` |
2415
- | `features` | `list`, `update` |
2416
- | `notification` | `send(workspaceId, event, userId?, data?)` |
2417
- | `permissions` | `check(workspaceId, userId, permission)`, `resolve(workspaceId, userId)` |
2511
+ | Module | Methods |
2512
+ | -------------- | ------------------------------------------------------------------------ |
2513
+ | `workspace` | `list`, `get`, `create`, `update`, `delete` |
2514
+ | `users` | `list`, `invite`, `remove`, `updateRole`, `getProfile`, `updateProfile` |
2515
+ | `subscription` | `get`, `checkout`, `update`, `cancel`, `resume`, `getBillingPortalUrl` |
2516
+ | `plans` | `getGroup`, `getVersions`, `getPublic`, `getVersion` |
2517
+ | `invoices` | `list`, `get` |
2518
+ | `usage` | `record`, `recordBatch`, `getQuota`, `getAll`, `getLogs` |
2519
+ | `settings` | `get` |
2520
+ | `features` | `list`, `update` |
2521
+ | `notification` | `send(workspaceId, event, userId?, data?)` |
2522
+ | `permissions` | `check(workspaceId, userId, permission)`, `resolve(workspaceId, userId)` |
2418
2523
 
2419
2524
  ### BuildBase Config Options
2420
2525