@buildbase/sdk 0.0.41 → 0.0.43

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
@@ -70,17 +70,17 @@ npm install @buildbase/sdk react@^19.0.0 react-dom@^19.0.0
70
70
 
71
71
  ```tsx
72
72
  // app/layout.tsx (or your root layout)
73
- import '@buildbase/sdk/css'
73
+ import '@buildbase/sdk/css';
74
74
  ```
75
75
 
76
76
  ### 2. Create Provider
77
77
 
78
78
  ```tsx
79
79
  // components/provider.tsx
80
- 'use client'
80
+ 'use client';
81
81
 
82
- import { SaaSOSProvider } from '@buildbase/sdk/react'
83
- import { ApiVersion } from '@buildbase/sdk'
82
+ import { SaaSOSProvider } from '@buildbase/sdk/react';
83
+ import { ApiVersion } from '@buildbase/sdk';
84
84
 
85
85
  export default function SaaSProvider({ children }: { children: React.ReactNode }) {
86
86
  return (
@@ -94,40 +94,40 @@ export default function SaaSProvider({ children }: { children: React.ReactNode }
94
94
  callbacks: {
95
95
  // Called on page refresh to restore session from httpOnly cookie
96
96
  getSession: async () => {
97
- const res = await fetch('/api/auth/session')
98
- const data = await res.json()
99
- return data.sessionId ?? null
97
+ const res = await fetch('/api/auth/session');
98
+ const data = await res.json();
99
+ return data.sessionId ?? null;
100
100
  },
101
101
 
102
102
  // Called after OAuth redirect to exchange code for sessionId
103
- handleAuthentication: async (code) => {
103
+ handleAuthentication: async code => {
104
104
  const res = await fetch('/api/auth/verify', {
105
105
  method: 'POST',
106
106
  headers: { 'Content-Type': 'application/json' },
107
107
  body: JSON.stringify({ code }),
108
- })
109
- const data = await res.json()
110
- return { sessionId: data.sessionId }
108
+ });
109
+ const data = await res.json();
110
+ return { sessionId: data.sessionId };
111
111
  },
112
112
 
113
113
  // Called on sign out to clear the httpOnly cookie
114
114
  onSignOut: async () => {
115
- await fetch('/api/auth/signout', { method: 'POST' })
116
- window.location.reload()
115
+ await fetch('/api/auth/signout', { method: 'POST' });
116
+ window.location.reload();
117
117
  },
118
118
 
119
119
  handleEvent: (eventType, data) => {
120
- console.log('SDK Event:', eventType, data)
120
+ console.log('SDK Event:', eventType, data);
121
121
  },
122
122
  onWorkspaceChange: async ({ workspace, user, role }) => {
123
- console.log('Switching to:', workspace.name, 'as', role)
123
+ console.log('Switching to:', workspace.name, 'as', role);
124
124
  },
125
125
  },
126
126
  }}
127
127
  >
128
128
  {children}
129
129
  </SaaSOSProvider>
130
- )
130
+ );
131
131
  }
132
132
  ```
133
133
 
@@ -141,8 +141,8 @@ The SDK uses the same session pattern as next-auth: the session token lives in a
141
141
 
142
142
  ```tsx
143
143
  // app/layout.tsx
144
- import SaaSProvider from '@/components/provider'
145
- import '@buildbase/sdk/css'
144
+ import SaaSProvider from '@/components/provider';
145
+ import '@buildbase/sdk/css';
146
146
 
147
147
  export default function RootLayout({ children }) {
148
148
  return (
@@ -151,7 +151,7 @@ export default function RootLayout({ children }) {
151
151
  <SaaSProvider>{children}</SaaSProvider>
152
152
  </body>
153
153
  </html>
154
- )
154
+ );
155
155
  }
156
156
  ```
157
157
 
@@ -253,13 +253,13 @@ const {
253
253
  Open the workspace settings dialog to a specific section:
254
254
 
255
255
  ```tsx
256
- openWorkspaceSettings('profile'); // Account profile
257
- openWorkspaceSettings('general'); // Workspace name, icon
258
- openWorkspaceSettings('users'); // Workspace members
256
+ openWorkspaceSettings('profile'); // Account profile
257
+ openWorkspaceSettings('general'); // Workspace name, icon
258
+ openWorkspaceSettings('users'); // Workspace members
259
259
  openWorkspaceSettings('subscription'); // Plan & Billing
260
- openWorkspaceSettings('usage'); // Quota usage dashboard
261
- openWorkspaceSettings('features'); // Feature toggles
262
- openWorkspaceSettings('danger'); // Delete workspace (owner only)
260
+ openWorkspaceSettings('usage'); // Quota usage dashboard
261
+ openWorkspaceSettings('features'); // Feature toggles
262
+ openWorkspaceSettings('danger'); // Delete workspace (owner only)
263
263
  ```
264
264
 
265
265
  ### Authentication Components
@@ -324,11 +324,11 @@ createCheckoutSession({
324
324
  });
325
325
  ```
326
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 |
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
332
 
333
333
  ## 👥 Role-Based Access Control
334
334
 
@@ -423,7 +423,11 @@ Control UI visibility based on the current workspace’s subscription. Subscript
423
423
  ### Subscription Gate Components
424
424
 
425
425
  ```tsx
426
- import { WhenSubscription, WhenNoSubscription, WhenSubscriptionToPlans } from '@buildbase/sdk/react';
426
+ import {
427
+ WhenSubscription,
428
+ WhenNoSubscription,
429
+ WhenSubscriptionToPlans,
430
+ } from '@buildbase/sdk/react';
427
431
 
428
432
  function BillingExample() {
429
433
  return (
@@ -538,10 +542,10 @@ function TrialExample() {
538
542
  }
539
543
  ```
540
544
 
541
- | Component | Renders when |
542
- | ----------------- | -------------------------------------------------------- |
543
- | `WhenTrialing` | Subscription status is `trialing` |
544
- | `WhenNotTrialing` | Subscription status is NOT `trialing` |
545
+ | Component | Renders when |
546
+ | ----------------- | --------------------------------------------------------------- |
547
+ | `WhenTrialing` | Subscription status is `trialing` |
548
+ | `WhenNotTrialing` | Subscription status is NOT `trialing` |
545
549
  | `WhenTrialEnding` | Trialing AND trial ends within `daysThreshold` days (default 3) |
546
550
 
547
551
  All trial gates support `loadingComponent` and `fallbackComponent` props.
@@ -567,13 +571,13 @@ function TrialInfo() {
567
571
  }
568
572
  ```
569
573
 
570
- | Property | Type | Description |
571
- | --------------- | -------------- | ------------------------------------------------ |
572
- | `isTrialing` | `boolean` | Whether subscription is in trial |
573
- | `daysRemaining` | `number` | Days left in trial (0 if not trialing or expired) |
574
- | `trialEndsAt` | `Date \| null` | Trial end date |
575
- | `trialStartedAt`| `Date \| null` | Trial start date |
576
- | `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 |
577
581
 
578
582
  ## 🔔 Push Notifications
579
583
 
@@ -582,7 +586,7 @@ Browser push notifications — built into the SDK. Users can enable/disable from
582
586
  **Only setup required:** Create `public/push-sw.js` in your app:
583
587
 
584
588
  ```js
585
- self.addEventListener('push', function(event) {
589
+ self.addEventListener('push', function (event) {
586
590
  if (!event.data) return;
587
591
  try {
588
592
  var payload = event.data.json();
@@ -601,17 +605,23 @@ self.addEventListener('push', function(event) {
601
605
  data: { url: payload.url, ...(payload.data || {}) },
602
606
  };
603
607
  event.waitUntil(self.registration.showNotification(payload.title || 'Notification', options));
604
- } catch (e) { console.error('[PushSW]', e); }
608
+ } catch (e) {
609
+ console.error('[PushSW]', e);
610
+ }
605
611
  });
606
612
 
607
- self.addEventListener('notificationclick', function(event) {
613
+ self.addEventListener('notificationclick', function (event) {
608
614
  event.notification.close();
609
615
  var url = event.notification.data && event.notification.data.url;
610
616
  if (url) {
611
- event.waitUntil(clients.matchAll({ type: 'window', includeUncontrolled: true }).then(function(list) {
612
- for (var i = 0; i < list.length; i++) { if (list[i].url === url && 'focus' in list[i]) return list[i].focus(); }
613
- if (clients.openWindow) return clients.openWindow(url);
614
- }));
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
+ );
615
625
  }
616
626
  });
617
627
  ```
@@ -629,16 +639,16 @@ Send email and push notifications to workspace members. The system has three lay
629
639
  ### Sending Notifications (Server-Side)
630
640
 
631
641
  ```ts
632
- import { notification } from '@/lib/buildbase'
642
+ import { notification } from '@/lib/buildbase';
633
643
 
634
644
  // Notify a specific user
635
645
  await notification.send(workspaceId, 'comment_added', userId, {
636
- title: 'New Comment', // Push title (falls back to event name)
646
+ title: 'New Comment', // Push title (falls back to event name)
637
647
  message: 'Alice commented on your project', // Push body + email {{message}}
638
648
  icon: 'https://example.com/comment-icon.png', // Custom push icon (falls back to org icon)
639
- 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
640
650
  url: 'https://app.example.com/projects/123#comments', // Opens on push click + {{url}} in email
641
- })
651
+ });
642
652
 
643
653
  // Notify all workspace members (omit userId)
644
654
  await notification.send(workspaceId, 'new_release', undefined, {
@@ -646,7 +656,7 @@ await notification.send(workspaceId, 'new_release', undefined, {
646
656
  message: 'Version 2.0 is now available with dark mode and API v2!',
647
657
  image: 'https://example.com/release-banner.jpg',
648
658
  url: 'https://app.example.com/changelog',
649
- })
659
+ });
650
660
  ```
651
661
 
652
662
  ### Ad-hoc Notifications
@@ -661,7 +671,7 @@ await notification.send(workspaceId, 'deployment_success', userId, {
661
671
  icon: 'https://example.com/deploy-icon.png',
662
672
  url: '/deployments/latest',
663
673
  channels: { push: true },
664
- })
674
+ });
665
675
  ```
666
676
 
667
677
  > **Note:** Ad-hoc events support push only. Email requires a registered event with a linked email template.
@@ -679,20 +689,20 @@ await notification.send(workspaceId, 'new_message', userId, {
679
689
  { action: 'reply', title: 'Reply', icon: 'https://example.com/reply.png' },
680
690
  { action: 'dismiss', title: 'Dismiss' },
681
691
  ],
682
- tag: 'chat-alice', // Replaces previous "chat-alice" notification instead of stacking
683
- 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
684
694
  channels: { push: true },
685
- })
695
+ });
686
696
 
687
697
  // Critical alert — stays visible until user interacts
688
698
  await notification.send(workspaceId, 'payment_failed', userId, {
689
699
  title: 'Payment Failed',
690
700
  message: 'Your subscription will be suspended in 3 days',
691
701
  badge: 'https://example.com/alert-badge.png',
692
- requireInteraction: true, // No auto-dismiss
693
- urgency: 'high', // Prioritized delivery on mobile
702
+ requireInteraction: true, // No auto-dismiss
703
+ urgency: 'high', // Prioritized delivery on mobile
694
704
  channels: { push: true },
695
- })
705
+ });
696
706
 
697
707
  // Silent notification — no sound or vibration
698
708
  await notification.send(workspaceId, 'sync_complete', userId, {
@@ -701,19 +711,19 @@ await notification.send(workspaceId, 'sync_complete', userId, {
701
711
  silent: true,
702
712
  urgency: 'low',
703
713
  channels: { push: true },
704
- })
714
+ });
705
715
  ```
706
716
 
707
- | Option | Type | Description |
708
- |--------|------|-------------|
709
- | `badge` | `string` | Small monochrome icon for status bar (Android/ChromeOS) |
710
- | `tag` | `string` | Replaces existing notification with same tag instead of stacking |
711
- | `actions` | `Array<{action, title, icon?}>` | Up to 2 action buttons on the notification |
712
- | `silent` | `boolean` | No sound or vibration |
713
- | `requireInteraction` | `boolean` | Stays visible until user interacts |
714
- | `renotify` | `boolean` | Sound/vibrate again when replacing via `tag` |
715
- | `timestamp` | `number` | Custom timestamp (ms since epoch) shown on notification |
716
- | `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 |
717
727
 
718
728
  ### Delivery Options
719
729
 
@@ -724,26 +734,26 @@ Control how and when the push service delivers the notification:
724
734
  await notification.send(workspaceId, 'daily_digest', undefined, {
725
735
  title: 'Your Daily Digest',
726
736
  message: '12 new updates in your workspace',
727
- scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
737
+ scheduledAt: '2026-04-16T09:00:00Z', // Deliver at 9am UTC tomorrow
728
738
  channels: { push: true },
729
- })
739
+ });
730
740
 
731
741
  // Short-lived notification — discard if not delivered in 1 hour
732
742
  await notification.send(workspaceId, 'flash_sale', undefined, {
733
743
  title: 'Flash Sale — 50% off!',
734
744
  message: 'Ends in 1 hour',
735
745
  image: 'https://example.com/sale-banner.jpg',
736
- ttl: 3600, // Expires after 1 hour (seconds)
737
- urgency: 'high', // Deliver ASAP
746
+ ttl: 3600, // Expires after 1 hour (seconds)
747
+ urgency: 'high', // Deliver ASAP
738
748
  channels: { push: true },
739
- })
749
+ });
740
750
  ```
741
751
 
742
- | Option | Type | Description |
743
- |--------|------|-------------|
744
- | `scheduledAt` | `string` | ISO 8601 date. Delays delivery until the specified time |
745
- | `ttl` | `number` | Time-to-live in seconds. Push service discards if not delivered in time. Default: 86400 (24h) |
746
- | `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 |
747
757
 
748
758
  ### Channel Control
749
759
 
@@ -754,19 +764,19 @@ By default, both email and push are sent (based on event config). Override per-s
754
764
  await notification.send(workspaceId, 'typing_indicator', userId, {
755
765
  message: 'Alice is typing...',
756
766
  channels: { push: true },
757
- })
767
+ });
758
768
 
759
769
  // Only email — digest or report, no push
760
770
  await notification.send(workspaceId, 'weekly_report', undefined, {
761
771
  message: 'Your weekly activity report is ready',
762
772
  channels: { email: true },
763
- })
773
+ });
764
774
 
765
775
  // Both channels explicitly
766
776
  await notification.send(workspaceId, 'comment_added', userId, {
767
777
  message: 'New comment on your project',
768
778
  channels: { email: true, push: true },
769
- })
779
+ });
770
780
  ```
771
781
 
772
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.
@@ -777,21 +787,21 @@ Both email and push support merge tags with `{{tag}}` syntax:
777
787
 
778
788
  ```ts
779
789
  await notification.send(workspaceId, 'export_ready', userId, {
780
- title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
790
+ title: '{{workspaceName}} — Export Ready', // → "Acme Corp — Export Ready"
781
791
  message: 'Hi {{name}}, your export is ready', // → "Hi Alice, your export is ready"
782
792
  downloadUrl: 'https://example.com/exports/123',
783
793
  fileName: 'report.csv',
784
- })
794
+ });
785
795
  ```
786
796
 
787
- | Tag | Resolves to | Available in |
788
- |-----|-------------|--------------|
789
- | `{{name}}` | Recipient's name | Email + Push |
790
- | `{{email}}` | Recipient's email | Email + Push |
791
- | `{{workspaceName}}` | Workspace name | Email + Push |
792
- | `{{message}}` | The `message` field | Email template |
793
- | `{{url}}` | The `url` field | Email template + Push click target |
794
- | `{{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 |
795
805
 
796
806
  ### Response
797
807
 
@@ -829,7 +839,7 @@ An email template is auto-created for each custom event. Edit it in **Email Temp
829
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.
830
840
 
831
841
  ```tsx
832
- import { SaaSOSProvider } from '@buildbase/sdk/react'
842
+ import { SaaSOSProvider } from '@buildbase/sdk/react';
833
843
 
834
844
  // The Notifications tab in workspace settings shows:
835
845
  // - Browser push toggle (subscribe/unsubscribe)
@@ -839,34 +849,34 @@ import { SaaSOSProvider } from '@buildbase/sdk/react'
839
849
  ### Types
840
850
 
841
851
  ```ts
842
- import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk'
852
+ import type { NotificationData, NotificationResult, NotificationEvent } from '@buildbase/sdk';
843
853
 
844
854
  interface NotificationData {
845
- title?: string; // Push title (falls back to event name)
846
- message?: string; // Push body + email {{message}}
847
- icon?: string; // Custom push icon URL (falls back to org icon)
848
- image?: string; // Large image in push notification body
849
- badge?: string; // Small monochrome status bar icon
850
- url?: string; // Opens on push click + {{url}} in email
851
- tag?: string; // Replace instead of stack notifications
852
- actions?: Array<{action, title, icon?}>; // Action buttons (max 2)
853
- silent?: boolean; // No sound/vibration
854
- requireInteraction?: boolean; // No auto-dismiss
855
- renotify?: boolean; // Re-alert on tag replace
856
- timestamp?: number; // Custom timestamp (ms)
857
- dir?: 'ltr' | 'rtl' | 'auto'; // Text direction
858
- ttl?: number; // Time-to-live (seconds)
859
- urgency?: 'very-low'|'low'|'normal'|'high'; // Delivery priority
860
- 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
861
871
  channels?: { email?: boolean; push?: boolean }; // Override which channels to use
862
- [key: string]: any; // Custom merge tags for email + push
872
+ [key: string]: any; // Custom merge tags for email + push
863
873
  }
864
874
 
865
875
  interface NotificationResult {
866
876
  sent: boolean;
867
877
  channels: { email: boolean; push: boolean };
868
878
  notifiedCount?: number;
869
- reason?: string; // Only when sent=false
879
+ reason?: string; // Only when sent=false
870
880
  }
871
881
 
872
882
  interface NotificationEvent {
@@ -885,23 +895,21 @@ The SDK supports 8 locales with ICU MessageFormat for plurals, selects, and numb
885
895
  ### Setup
886
896
 
887
897
  ```tsx
888
- <SaaSOSProvider locale="hi">
889
- {/* All SDK UI renders in Hindi */}
890
- </SaaSOSProvider>
898
+ <SaaSOSProvider locale="hi">{/* All SDK UI renders in Hindi */}</SaaSOSProvider>
891
899
  ```
892
900
 
893
901
  ### Supported Locales
894
902
 
895
- | Code | Language | Numerals | Direction |
896
- |---|---|---|---|
897
- | `en` | English | 1,234.56 | LTR |
898
- | `es` | Spanish | 1.234,56 | LTR |
899
- | `fr` | French | 1 234,56 | LTR |
900
- | `de` | German | 1.234,56 | LTR |
901
- | `ja` | Japanese | 1,234.56 | LTR |
902
- | `zh` | Chinese | 1,234.56 | LTR |
903
- | `hi` | Hindi | Devanagari (e.g. 1,234) | LTR |
904
- | `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 |
905
913
 
906
914
  ### useTranslation Hook
907
915
 
@@ -913,10 +921,10 @@ function MyComponent() {
913
921
 
914
922
  return (
915
923
  <div dir={dir}>
916
- <p>{t('subscription.currentPlan')}</p> {/* Type-safe key lookup */}
917
- <p>{t('users.memberCount', { count: 5 })}</p> {/* ICU plural: "5 members" */}
918
- <p>{fmtNum(1234)}</p> {/* Locale-aware: "1,234" or "1,234" */}
919
- <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$" */}
920
928
  </div>
921
929
  );
922
930
  }
@@ -958,13 +966,13 @@ For full SaaS platforms (Slack, GitHub, Discord):
958
966
 
959
967
  Platform mode supports granular overrides from the admin dashboard:
960
968
 
961
- | Setting | Options | Default |
962
- |---|---|---|
963
- | Can Create Workspace | Everyone / Owner Only / Disabled | Everyone |
964
- | Can Invite Members | Everyone / Admin Only / Disabled | Everyone |
965
- | Show Workspace Switcher | On / Off | On |
966
- | Max Workspaces Per User | 0 (unlimited) or a number | 0 |
967
- | 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 |
968
976
 
969
977
  These let you achieve team-like, managed, or enterprise-like behavior without a separate mode.
970
978
 
@@ -1113,16 +1121,17 @@ function PublicPricingPage() {
1113
1121
  ```
1114
1122
 
1115
1123
  `selectPlan()` handles everything automatically:
1124
+
1116
1125
  - **Authenticated** → opens the "Choose Your Plan" dialog
1117
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
1118
1127
 
1119
- | Prop | Type | Description |
1120
- | ----------------- | ------------------------------ | ----------------------------------------------------------------- |
1121
- | `slug` | `string` | Plan group slug (e.g. 'main-pricing', 'enterprise') |
1122
- | `children` | `(details) => ReactNode` | Render prop receiving plan details (see below) |
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) |
1123
1132
  | `redirectBaseUrl` | `string` | Base URL for post-login redirects (e.g. `"https://app.com/dashboard"`). Enables `selectPlan()` for unauthenticated users. |
1124
- | `loadingFallback` | `ReactNode` | Custom loading UI (defaults to skeleton) |
1125
- | `errorFallback` | `(error: string) => ReactNode` | Custom error UI |
1133
+ | `loadingFallback` | `ReactNode` | Custom loading UI (defaults to skeleton) |
1134
+ | `errorFallback` | `(error: string) => ReactNode` | Custom error UI |
1126
1135
 
1127
1136
  **Render prop details**: `{ loading, error, items, plans, notes, refetch, selectPlan }`
1128
1137
 
@@ -1138,41 +1147,41 @@ Plans support **pricing variants** (multi-currency). Use these utilities for dis
1138
1147
 
1139
1148
  ### Currency utilities
1140
1149
 
1141
- | Export | Purpose |
1142
- |--------|--------|
1143
- | `CURRENCY_DISPLAY` | Map of currency code → symbol (e.g. `usd` → `$`) |
1144
- | `CURRENCY_FLAG` | Map of currency code → flag emoji |
1145
- | `PLAN_CURRENCY_CODES` | Allowed billing currency codes (for dropdowns/validation) |
1146
- | `PLAN_CURRENCY_OPTIONS` | Options array for plan currency selects |
1147
- | `getCurrencySymbol(currency)` | Symbol for a Stripe currency code |
1148
- | `getCurrencyFlag(currency)` | Flag emoji for a currency code |
1149
- | `formatCents(cents, currency)` | Format cents as localized price string |
1150
- | `formatOverageRate(cents, currency)` | Format overage rate for display |
1151
- | `formatOverageRateWithLabel(...)` | Overage rate with optional unit label |
1152
- | `formatQuotaIncludedOverage(...)` | "X included, then $Y / unit" style text |
1153
- | `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 |
1154
1163
 
1155
1164
  ### Pricing variant utilities
1156
1165
 
1157
- | Export | Purpose |
1158
- |--------|--------|
1159
- | `getPricingVariant(planVersion, currency)` | Get variant for a currency, or `null` |
1160
- | `getBasePriceCents(planVersion, currency, interval)` | Base price in cents for currency/interval |
1161
- | `getStripePriceIdForInterval(planVersion, currency, interval)` | Stripe price ID for checkout |
1162
- | `getQuotaOverageCents(planVersion, currency, quotaSlug, interval)` | Overage cents for a quota |
1163
- | `getQuotaDisplayWithVariant(planVersion, currency, quotaSlug, interval)` | Display value with overage for a variant |
1164
- | `getAvailableCurrenciesFromPlans(plans)` | Unique currency codes across plan versions |
1165
- | `getDisplayCurrency(planVersion, currency)` | Display currency (variant exists ? currency : plan.currency) |
1166
- | `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 |
1167
1176
 
1168
1177
  Types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage`.
1169
1178
 
1170
1179
  ### Quota utilities
1171
1180
 
1172
- | Export | Purpose |
1173
- |--------|--------|
1181
+ | Export | Purpose |
1182
+ | -------------------------------------------------- | ------------------------------------------------------------------- |
1174
1183
  | `getQuotaDisplayValue(quotaByInterval, interval?)` | Normalize `IQuotaByInterval` to `{ included, overage?, unitSize? }` |
1175
- | `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" |
1176
1185
 
1177
1186
  Types: `QuotaDisplayValue`, `FormatQuotaWithPriceOptions`. Plan/subscription types use `IQuotaByInterval` and `IQuotaIntervalValue` for per-interval quotas and overages.
1178
1187
 
@@ -1204,12 +1213,12 @@ Track and monitor metered usage for subscription quotas (e.g., API calls, emails
1204
1213
 
1205
1214
  ### When to use which?
1206
1215
 
1207
- | Scenario | Where to record | Why |
1208
- |----------|----------------|-----|
1216
+ | Scenario | Where to record | Why |
1217
+ | ------------------------------- | ---------------------- | -------------------------------------------- |
1209
1218
  | User clicks "Send Email" button | Client-side (SDK hook) | User-initiated, immediate UI feedback needed |
1210
- | API request hits your backend | Server-side (REST API) | Backend controls the resource, more secure |
1211
- | Background job processes data | Server-side (REST API) | No browser context available |
1212
- | 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 |
1213
1222
 
1214
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.
1215
1224
 
@@ -1233,8 +1242,8 @@ function SendEmailButton() {
1233
1242
  const result = await recordUsage({
1234
1243
  quotaSlug: 'emails',
1235
1244
  quantity: 1,
1236
- source: 'web-app', // optional: track where usage came from
1237
- 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
1238
1247
  });
1239
1248
  console.log(`Used: ${result.consumed}/${result.included}, Available: ${result.available}`);
1240
1249
  if (result.overage > 0) {
@@ -1245,7 +1254,11 @@ function SendEmailButton() {
1245
1254
  }
1246
1255
  };
1247
1256
 
1248
- return <button onClick={handleSend} disabled={loading}>Send Email</button>;
1257
+ return (
1258
+ <button onClick={handleSend} disabled={loading}>
1259
+ Send Email
1260
+ </button>
1261
+ );
1249
1262
  }
1250
1263
  ```
1251
1264
 
@@ -1265,7 +1278,9 @@ function QuotaStatusBar({ quotaSlug }: { quotaSlug: string }) {
1265
1278
 
1266
1279
  return (
1267
1280
  <div>
1268
- <p>{quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)</p>
1281
+ <p>
1282
+ {quotaSlug}: {status.consumed} / {status.included} ({usagePercent}%)
1283
+ </p>
1269
1284
  <p>Available: {status.available}</p>
1270
1285
  {status.hasOverage && <p>Overage: {status.overage} units</p>}
1271
1286
  <button onClick={refetch}>Refresh</button>
@@ -1306,12 +1321,10 @@ import { useUsageLogs, useSaaSWorkspaces } from '@buildbase/sdk/react';
1306
1321
 
1307
1322
  function UsageLogsTable() {
1308
1323
  const { currentWorkspace } = useSaaSWorkspaces();
1309
- const {
1310
- logs, totalDocs, totalPages, page, hasNextPage, loading, refetch,
1311
- } = useUsageLogs(
1324
+ const { logs, totalDocs, totalPages, page, hasNextPage, loading, refetch } = useUsageLogs(
1312
1325
  currentWorkspace?._id,
1313
- 'api_calls', // optional: filter by quota slug
1314
- { 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
1315
1328
  );
1316
1329
 
1317
1330
  if (loading) return <Spinner />;
@@ -1320,7 +1333,12 @@ function UsageLogsTable() {
1320
1333
  <div>
1321
1334
  <table>
1322
1335
  <thead>
1323
- <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>
1324
1342
  </thead>
1325
1343
  <tbody>
1326
1344
  {logs.map(log => (
@@ -1333,7 +1351,9 @@ function UsageLogsTable() {
1333
1351
  ))}
1334
1352
  </tbody>
1335
1353
  </table>
1336
- <p>Page {page} of {totalPages} ({totalDocs} total)</p>
1354
+ <p>
1355
+ Page {page} of {totalPages} ({totalDocs} total)
1356
+ </p>
1337
1357
  </div>
1338
1358
  );
1339
1359
  }
@@ -1341,23 +1361,23 @@ function UsageLogsTable() {
1341
1361
 
1342
1362
  **`useUsageLogs` parameters:**
1343
1363
 
1344
- | Parameter | Type | Required | Description |
1345
- |-----------|------|----------|-------------|
1346
- | `workspaceId` | `string \| null \| undefined` | Yes | Workspace ID (null/undefined disables fetching) |
1347
- | `quotaSlug` | `string` | No | Filter logs by quota slug |
1348
- | `options.from` | `string` | No | ISO date string — filter logs from this date |
1349
- | `options.to` | `string` | No | ISO date string — filter logs until this date |
1350
- | `options.source` | `string` | No | Filter logs by source |
1351
- | `options.page` | `number` | No | Page number (default: 1) |
1352
- | `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) |
1353
1373
 
1354
1374
  #### Client-Side Hooks Summary
1355
1375
 
1356
- | Hook | Purpose |
1357
- |------|---------|
1358
- | `useRecordUsage(workspaceId)` | Record quota consumption (mutation) |
1359
- | `useQuotaUsageStatus(workspaceId, quotaSlug)` | Get single quota status (auto-fetches) |
1360
- | `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) |
1361
1381
  | `useUsageLogs(workspaceId, quotaSlug?, options?)` | Get paginated usage history (auto-fetches) |
1362
1382
 
1363
1383
  ---
@@ -1404,8 +1424,8 @@ async function recordUsage(workspaceId: string, quotaSlug: string, quantity: num
1404
1424
  body: JSON.stringify({
1405
1425
  quotaSlug,
1406
1426
  quantity,
1407
- source: 'backend', // optional: helps distinguish from client-side usage
1408
- metadata: {}, // optional: attach custom data
1427
+ source: 'backend', // optional: helps distinguish from client-side usage
1428
+ metadata: {}, // optional: attach custom data
1409
1429
  idempotencyKey: undefined, // optional: prevent duplicate recordings
1410
1430
  }),
1411
1431
  });
@@ -1443,10 +1463,9 @@ async function getQuotaStatus(workspaceId: string, quotaSlug: string) {
1443
1463
 
1444
1464
  // Get status for ALL quotas
1445
1465
  async function getAllQuotaStatus(workspaceId: string) {
1446
- const response = await fetch(
1447
- `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`,
1448
- { headers: { 'x-session-id': SESSION_ID } }
1449
- );
1466
+ const response = await fetch(`${BASE_URL}/workspaces/${workspaceId}/subscription/usage/all`, {
1467
+ headers: { 'x-session-id': SESSION_ID },
1468
+ });
1450
1469
  return response.json();
1451
1470
  // Returns: { quotas: { [slug]: { consumed, included, available, overage, hasOverage } } }
1452
1471
  }
@@ -1483,26 +1502,23 @@ await bb.usage.recordBatch(workspaceId, {
1483
1502
  { quotaSlug: 'images', quantity: 500, source: 'batch-export-job' },
1484
1503
  { quotaSlug: 'videos', quantity: 10, source: 'batch-export-job' },
1485
1504
  { quotaSlug: 'images', quantity: 200, metadata: { jobId: 'abc123' } },
1486
- ]
1505
+ ],
1487
1506
  });
1488
1507
  // Returns: { success, total: 3, succeeded: 3, failed: 0, results: [...] }
1489
1508
  ```
1490
1509
 
1491
1510
  ```ts
1492
1511
  // Using REST API directly
1493
- const response = await fetch(
1494
- `${BASE_URL}/workspaces/${workspaceId}/subscription/usage/batch`,
1495
- {
1496
- method: 'POST',
1497
- headers: { 'x-session-id': SESSION_ID, 'Content-Type': 'application/json' },
1498
- body: JSON.stringify({
1499
- items: [
1500
- { quotaSlug: 'images', quantity: 500, source: 'batch-export' },
1501
- { quotaSlug: 'videos', quantity: 10 },
1502
- ]
1503
- }),
1504
- }
1505
- );
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
+ });
1506
1522
  ```
1507
1523
 
1508
1524
  - Max **100 items** per request
@@ -1512,35 +1528,35 @@ const response = await fetch(
1512
1528
 
1513
1529
  #### Server-Side API Reference
1514
1530
 
1515
- | Endpoint | Method | Description |
1516
- |----------|--------|-------------|
1517
- | `/api/v1/public/token/exchange` | POST | Exchange API token for session ID |
1518
- | `/api/v1/public/workspaces/:id/subscription/usage` | POST | Record quota usage |
1519
- | `/api/v1/public/workspaces/:id/subscription/usage/status?quotaSlug=X` | GET | Get single quota status |
1520
- | `/api/v1/public/workspaces/:id/subscription/usage/all` | GET | Get all quotas status |
1521
- | `/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 |
1522
1538
 
1523
1539
  All endpoints (except `/token/exchange`) require the `x-session-id` header.
1524
1540
 
1525
1541
  **Record usage request body:**
1526
1542
 
1527
- | Field | Type | Required | Description |
1528
- |-------|------|----------|-------------|
1529
- | `quotaSlug` | `string` | Yes | Quota identifier (e.g. `'api_calls'`, `'emails'`, `'storage'`) |
1530
- | `quantity` | `number` | Yes | Units to consume (minimum 1) |
1531
- | `metadata` | `object` | No | Custom metadata to attach to the usage record |
1532
- | `source` | `string` | No | Source identifier (e.g. `'backend'`, `'worker'`, `'cron'`) |
1533
- | `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 |
1534
1550
 
1535
1551
  **Record usage response:**
1536
1552
 
1537
- | Field | Type | Description |
1538
- |-------|------|-------------|
1539
- | `used` | `number` | Quantity recorded in this request |
1540
- | `consumed` | `number` | Total usage in the current billing period |
1541
- | `included` | `number` | Total units included in the plan |
1542
- | `available` | `number` | Remaining units before overage |
1543
- | `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 |
1544
1560
  | `billedAsync` | `boolean` | Whether overage billing was queued to Stripe |
1545
1561
 
1546
1562
  ## 🚦 Quota Gates
@@ -1609,12 +1625,12 @@ All quota gate components support optional `loadingComponent` and `fallbackCompo
1609
1625
 
1610
1626
  ### Quota Gate Components Reference
1611
1627
 
1612
- | Component | Renders children when | Props |
1613
- |-----------|----------------------|-------|
1614
- | `WhenQuotaAvailable` | Quota has remaining units (`available > 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1615
- | `WhenQuotaExhausted` | Quota is fully consumed (`available <= 0`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1616
- | `WhenQuotaOverage` | Usage exceeds included amount (`hasOverage`) | `slug`, `children`, `loadingComponent?`, `fallbackComponent?` |
1617
- | `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?` |
1618
1634
 
1619
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.
1620
1636
 
@@ -1634,7 +1650,9 @@ function QuotaDebug() {
1634
1650
  return (
1635
1651
  <div>
1636
1652
  {Object.entries(quotas).map(([slug, usage]) => (
1637
- <p key={slug}>{slug}: {usage.consumed}/{usage.included}</p>
1653
+ <p key={slug}>
1654
+ {slug}: {usage.consumed}/{usage.included}
1655
+ </p>
1638
1656
  ))}
1639
1657
  <button onClick={() => refetch()}>Refresh</button>
1640
1658
  </div>
@@ -1642,13 +1660,14 @@ function QuotaDebug() {
1642
1660
  }
1643
1661
  ```
1644
1662
 
1645
- | Property | Type | Description |
1646
- |----------|------|-------------|
1647
- | `quotas` | `Record<string, IQuotaUsageStatus> \| null` | Current quota usage data keyed by slug |
1648
- | `loading` | `boolean` | True while quota data is being fetched |
1649
- | `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 |
1650
1668
 
1651
1669
  **When quota usage refetches:**
1670
+
1652
1671
  - When the current workspace changes (automatic).
1653
1672
  - When usage is recorded via `useRecordUsage` — refetch is triggered automatically.
1654
1673
  - When you call `refetch()` manually.
@@ -1751,25 +1770,25 @@ All SDK API clients extend a shared base class and are exported from the package
1751
1770
  | `BaseApi` | Abstract base (URL, auth, `fetchJson`/`fetchResponse`) – extend for custom APIs |
1752
1771
  | `IBaseApiConfig` | Config type: `serverUrl`, `version`, optional `orgId` |
1753
1772
  | `UserApi` | User attributes and features |
1754
- | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
1773
+ | `WorkspaceApi` | Workspaces, subscription, invoices, quota usage, users |
1755
1774
  | `SettingsApi` | Organization settings |
1756
1775
 
1757
1776
  ### Components
1758
1777
 
1759
- | Component | Purpose |
1760
- | --- | --- |
1761
- | `SubscriptionContextProvider` | Provides subscription data to children (included in SaaSOSProvider) |
1762
- | `WhenSubscription`, `WhenNoSubscription`, `WhenSubscriptionToPlans` | Subscription gate components |
1763
- | `WhenTrialing`, `WhenNotTrialing`, `WhenTrialEnding` | Trial gate components |
1764
- | `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 |
1765
1784
 
1766
1785
  ### Currency, pricing variant & quota utilities
1767
1786
 
1768
- | Category | Exports |
1769
- | -------- | ------- |
1770
- | **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` |
1771
1790
  | **Pricing variants** | `getPricingVariant`, `getBasePriceCents`, `getStripePriceIdForInterval`, `getQuotaOverageCents`, `getQuotaDisplayWithVariant`, `getAvailableCurrenciesFromPlans`, `getDisplayCurrency`, `getBillingIntervalAndCurrencyFromPriceId`; types: `IPricingVariant`, `PlanVersionWithPricingVariants`, `QuotaDisplayWithOverage` |
1772
- | **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. |
1773
1792
 
1774
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.):
1775
1794
 
@@ -1790,21 +1809,21 @@ const workspaceApi = new WorkspaceApi({
1790
1809
 
1791
1810
  Prefer these SDK hooks for state and operations instead of `useAppSelector`:
1792
1811
 
1793
- | Hook | Purpose |
1794
- | -------------------------- | ------------------------------------------------------------------------------------------------------- |
1795
- | `useSaaSAuth()` | Auth state (user, session, status), signIn(returnUrl?), signOut, openWorkspaceSettings |
1796
- | `useSaaSWorkspaces()` | Workspaces, currentWorkspace, loading, switching/switchingToId, CRUD and switch actions |
1797
- | `useSaaSOs()` | OS config (serverUrl, version, orgId, auth, settings) when you need the full config object |
1798
- | `useSaaSSettings()` | Organization settings and getSettings (prefer this when you only need settings) |
1799
- | `useUserAttributes()` | User attributes and update/refresh |
1800
- | `useUserFeatures()` | User feature flags |
1801
- | `useSubscriptionContext()` | Subscription for current workspace (response, loading, refetch); use inside SubscriptionContextProvider |
1802
- | `useTrialStatus()` | Trial state: `isTrialing`, `daysRemaining`, `trialEndsAt`, `isTrialEnding` |
1803
- | `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()` |
1804
1823
  | Subscription hooks | `usePublicPlans`, `useSubscription`, `useSubscriptionManagement`, `usePlanGroup`, `usePlanGroupVersions`, `usePublicPlanGroupVersion`, `useCreateCheckoutSession`, `useUpdateSubscription`, `useCancelSubscription`, `useResumeSubscription`, `useInvoices`, `useInvoice` |
1805
- | `useQuotaUsageContext()` | Quota usage for current workspace (quotas, loading, refetch); use inside QuotaUsageContextProvider |
1806
- | Quota usage hooks | `useRecordUsage`, `useQuotaUsageStatus`, `useAllQuotaUsage`, `useUsageLogs` |
1807
- | 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 |
1808
1827
 
1809
1828
  Using hooks keeps your code stable if internal state shape changes and avoids direct Redux/context coupling.
1810
1829
 
@@ -1862,6 +1881,7 @@ interface OnWorkspaceChangeParams {
1862
1881
  ```
1863
1882
 
1864
1883
  **Session flow** (same pattern as next-auth):
1884
+
1865
1885
  - Session token is stored in an **httpOnly cookie** (set by your server, not readable by JS)
1866
1886
  - On page refresh, the SDK calls `getSession()` once to restore the session
1867
1887
  - Session data (user info) lives **in-memory only** (React context) — no localStorage
@@ -1874,16 +1894,16 @@ interface OnWorkspaceChangeParams {
1874
1894
 
1875
1895
  ### BetaForm Props
1876
1896
 
1877
- | Prop | Type | Default | Description |
1878
- | ------------------------ | ------------------------------------------------------ | -------------------------------- | --------------------------------------- |
1879
- | `onSuccess` | `() => void` | - | Callback when form submits successfully |
1880
- | `onError` | `(error: string) => void` | - | Callback when form submission fails |
1881
- | `className` | `string` | `'w-full'` | CSS class for form container |
1882
- | `fieldClassName` | `string` | `'flex flex-col gap-1.5 w-full'` | CSS class for form fields |
1883
- | `autoFocus` | `boolean` | `true` | Auto-focus name field |
1884
- | `showSuccessMessage` | `boolean` | `true` | Show success message after submit |
1885
- | `hideLogo` | `boolean` | `false` | Hide logo |
1886
- | `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 |
1887
1907
 
1888
1908
  > **Note:** The form language is inherited from the `SaaSOSProvider` locale setting. Supported locales: en, es, fr, de, ja, zh, hi, ar.
1889
1909
 
@@ -1983,7 +2003,11 @@ function Dashboard() {
1983
2003
  ### Pattern 4b: Subscription-Gated UI
1984
2004
 
1985
2005
  ```tsx
1986
- import { WhenSubscription, WhenNoSubscription, WhenSubscriptionToPlans } from '@buildbase/sdk/react';
2006
+ import {
2007
+ WhenSubscription,
2008
+ WhenNoSubscription,
2009
+ WhenSubscriptionToPlans,
2010
+ } from '@buildbase/sdk/react';
1987
2011
 
1988
2012
  function BillingPage() {
1989
2013
  return (
@@ -2023,7 +2047,9 @@ function FeatureWithQuota() {
2023
2047
  </WhenQuotaAvailable>
2024
2048
 
2025
2049
  <WhenQuotaExhausted slug="api_calls">
2026
- <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>
2027
2053
  </WhenQuotaExhausted>
2028
2054
  </div>
2029
2055
  );
@@ -2385,31 +2411,45 @@ Configure once, use everywhere. Same pattern as Auth.js.
2385
2411
 
2386
2412
  ```ts
2387
2413
  // lib/buildbase.ts
2388
- import BuildBase from '@buildbase/sdk'
2389
- import { cookies } from 'next/headers'
2390
-
2391
- 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({
2392
2432
  serverUrl: process.env.BUILDBASE_URL!,
2393
2433
  orgId: process.env.BUILDBASE_ORG_ID!,
2394
2434
  getSessionId: async () => {
2395
- const c = await cookies()
2396
- return c.get('bb-session-id')?.value ?? null
2435
+ const c = await cookies();
2436
+ return c.get('bb-session-id')?.value ?? null;
2397
2437
  },
2398
- })
2438
+ });
2399
2439
  ```
2400
2440
 
2401
2441
  Use in API routes — session is resolved automatically:
2402
2442
 
2403
2443
  ```ts
2404
2444
  // app/api/workspace/route.ts
2405
- import { auth, workspace, subscription } from '@/lib/buildbase'
2445
+ import { auth, workspace, subscription } from '@/lib/buildbase';
2406
2446
 
2407
2447
  export async function GET() {
2408
- const session = await auth()
2409
- if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 })
2448
+ const session = await auth();
2449
+ if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });
2410
2450
 
2411
- const workspaces = await workspace.list()
2412
- return Response.json({ workspaces })
2451
+ const workspaces = await workspace.list();
2452
+ return Response.json({ workspaces });
2413
2453
  }
2414
2454
  ```
2415
2455
 
@@ -2419,31 +2459,31 @@ No `getSessionId` callback — use `withSession()` per-request instead.
2419
2459
 
2420
2460
  ```ts
2421
2461
  // buildbase.ts
2422
- import BuildBase from '@buildbase/sdk'
2462
+ import BuildBase from '@buildbase/sdk';
2423
2463
 
2424
2464
  const bb = BuildBase({
2425
2465
  serverUrl: process.env.BUILDBASE_URL!,
2426
2466
  orgId: process.env.BUILDBASE_ORG_ID!,
2427
- })
2467
+ });
2428
2468
 
2429
- export const { withSession, plans } = bb
2469
+ export const { withSession, plans } = bb;
2430
2470
  ```
2431
2471
 
2432
2472
  ```ts
2433
2473
  // routes
2434
2474
  app.get('/workspaces', async (req, res) => {
2435
- const { workspace } = withSession(req.headers['x-session-id'])
2436
- res.json(await workspace.list())
2437
- })
2475
+ const { workspace } = withSession(req.headers['x-session-id']);
2476
+ res.json(await workspace.list());
2477
+ });
2438
2478
 
2439
2479
  app.post('/usage', async (req, res) => {
2440
- const { usage } = withSession(req.headers['x-session-id'])
2480
+ const { usage } = withSession(req.headers['x-session-id']);
2441
2481
  const result = await usage.record(req.body.workspaceId, {
2442
2482
  quotaSlug: 'api-calls',
2443
2483
  quantity: 1,
2444
- })
2445
- res.json(result)
2446
- })
2484
+ });
2485
+ res.json(result);
2486
+ });
2447
2487
  ```
2448
2488
 
2449
2489
  ### Background Jobs / Webhooks
@@ -2451,35 +2491,35 @@ app.post('/usage', async (req, res) => {
2451
2491
  For service accounts (no user session), use `withSession()` with a service token:
2452
2492
 
2453
2493
  ```ts
2454
- import { withSession } from '@/lib/buildbase'
2494
+ import { withSession } from '@/lib/buildbase';
2455
2495
 
2456
- const bb = withSession(process.env.SERVICE_SESSION_ID!)
2496
+ const bb = withSession(process.env.SERVICE_SESSION_ID!);
2457
2497
 
2458
2498
  // Record usage from a webhook
2459
2499
  await bb.usage.record(workspaceId, {
2460
2500
  quotaSlug: 'uploads',
2461
2501
  quantity: 1,
2462
2502
  source: 'webhook:file.processed',
2463
- })
2503
+ });
2464
2504
 
2465
2505
  // Check subscription
2466
- const sub = await bb.subscription.get(workspaceId)
2506
+ const sub = await bb.subscription.get(workspaceId);
2467
2507
  ```
2468
2508
 
2469
2509
  ### Server-Side Actions Reference
2470
2510
 
2471
- | Module | Methods |
2472
- |--------|---------|
2473
- | `workspace` | `list`, `get`, `create`, `update`, `delete` |
2474
- | `users` | `list`, `invite`, `remove`, `updateRole`, `getProfile`, `updateProfile` |
2475
- | `subscription` | `get`, `checkout`, `update`, `cancel`, `resume`, `getBillingPortalUrl` |
2476
- | `plans` | `getGroup`, `getVersions`, `getPublic`, `getVersion` |
2477
- | `invoices` | `list`, `get` |
2478
- | `usage` | `record`, `recordBatch`, `getQuota`, `getAll`, `getLogs` |
2479
- | `settings` | `get` |
2480
- | `features` | `list`, `update` |
2481
- | `notification` | `send(workspaceId, event, userId?, data?)` |
2482
- | `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)` |
2483
2523
 
2484
2524
  ### BuildBase Config Options
2485
2525