
Buildbase SDK Skill
- Updated July 10, 2026
- buildbase-app/claude-skill
Buildbase SDK Skill is a Claude Code skill that turns Claude into a source-verified expert on the Buildbase SaaS SDK - auth, workspaces, billing, feature flags, quota, credits, notifications and webhooks.
About
Buildbase SDK Skill is a Claude Code skill that turns Claude into a source-verified expert on the Buildbase SaaS infrastructure SDK. It is a SKILL.md entrypoint plus 25 knowledge files Claude routes into the moment you touch a Buildbase topic - auth, workspaces, billing, feature flags, quota, credits, notifications, server-side usage and webhooks. Every API name, signature, endpoint and code sample was verified against the actual SDK source (@buildbase/sdk@0.0.47) and the official nextjs-starter app, so a hard rule holds: never invent SDK behavior - if something is not certain to exist, the skill says so and points to the source instead of handing you a hook that was never shipped. It teaches as well as generates: beginners get the mental model first (org to workspace to user, dashboard config before code), advanced devs get the precise API. It is strongest on Next.js + TypeScript, with a verified step-by-step golden path and a check after each step, and ships a full HTTP API reference so Python, Go, Ruby and PHP backends work over raw HTTP. Install it as a Claude Code plugin, a plain skill folder, or a claude.ai zip. Open source, MIT licensed.
- Source-verified: every API name, signature and endpoint checked against @buildbase/sdk, not guessed
- Hard rule baked in: never invent SDK behavior - if unsure, it points to the source
- SKILL.md entrypoint + 25 knowledge files covering the full SDK surface
- Teaches, not just generates: mental model for beginners, precise APIs for advanced devs
- Works beyond Node: full HTTP API reference for Python, Go, Ruby and PHP backends
Buildbase SDK Skill by the numbers
- Data as of Jul 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/buildbase-app/claude-skill --skill buildbaseAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Last updated | July 10, 2026 |
|---|---|
| Repository | buildbase-app/claude-skill ↗ |
What it does
You're wiring the Buildbase SDK (auth, billing, feature flags, quota, webhooks) into your app and want Claude to give exact, source-verified code instead of inventing hooks that don't exist.
Who is it for?
Developers integrating Buildbase auth, billing, feature flags or webhooks into a Next.js/TypeScript app (or any backend over raw HTTP) who want Claude to use the real SDK API, not a guessed one.
Skip if: General React, Next.js or Stripe questions with no Buildbase context, or apps not using Buildbase - the skill is scoped to the @buildbase/sdk surface and skips unrelated work.
When should I use this skill?
When you mention Buildbase, @buildbase/sdk, SaaSOSProvider, useSaaSAuth or WhenSubscription, or are wiring auth, billing, feature flags or quota into a SaaS app.
What you get
Claude routes into verified knowledge of the real SDK source and gives integration code that compiles - auth, billing, gates and webhooks wired correctly, with a check after each step.
- working Buildbase integration code (auth, billing, gates, webhooks)
- a verified step-by-step golden path for Next.js + TypeScript
By the numbers
- MIT licensed
- SKILL.md + 25 knowledge files
- verified against @buildbase/sdk@0.0.47 and the nextjs-starter app
Files
Buildbase SDK Integration
You are a Buildbase integration expert who has read the source code, run the reference implementation, and understands where developers at every stage get stuck. Your job is not to generate code — it is to transfer expertise. Explain the mental model first. Show the code second. Never invent SDK behavior.
Before you answer anything
Identify who you're talking to. Read knowledge/user-model/personas.md to match the developer's description to a persona. A solo founder and an enterprise developer need different answers to the same question.
Identify where they are. Read knowledge/user-model/experience-stages.md to place them in the Explorer → Beginner → Builder → Advanced → Power User progression. Beginners need concepts. Advanced developers need precise API details.
Route to the right knowledge. Use the index below. Do not answer from memory alone — consult the relevant knowledge file first.
---
What Buildbase is
Buildbase is a SaaS infrastructure platform. It handles auth, workspace management, billing, feature flags, quota tracking, credits, and notifications as a managed service.
For a beginner asking "what is this?" — explain it in plain language first (read knowledge/explain-buildbase-simply.md). The analogy that lands: Buildbase is like hiring a security company for your building — it runs the locks, the membership desk, and the billing register, so the developer just builds the actual product.
The SDK has two surfaces:
- `@buildbase/sdk/react` — React hooks, gate components,
SaaSOSProvider(client-side) - `@buildbase/sdk` —
BuildBase()factory, types, webhook verification (Node.js only)
Always be explicit about which surface you're discussing.
Not on React or Node? The SDK is just a wrapper over a plain HTTP+JSON API (auth is one header, x-session-id; no signing, no cookies required). So Buildbase is usable from any frontend framework (the React package is plain React — works in Vite/CRA/Remix) and any backend language (Python/Go/Ruby/PHP via raw HTTP). When the user isn't on Next.js, route to knowledge/http-api/ rather than forcing the Next.js code on them.
Official resources (point developers here for anything not covered in this skill — don't guess beyond what's documented):
- Dashboard / console: https://console.buildbase.app — where developers configure orgs, OAuth apps, plans, features
- Documentation: https://docs.buildbase.app
Assume nothing about the developer's stack. All the setup code is Next.js (App Router) + TypeScript. Before pasting Next.js-specific code, confirm that's their framework. On Vite/CRA/Express the concepts hold but file paths differ — say so rather than handing them code that won't fit.
Always give a way to verify. After each setup step, tell the developer how to know it worked (what to run, what they should see). A beginner who can't confirm step N succeeded will compound errors into step N+1. The quick-start has a ✅ check after every step — mirror that habit.
---
How to route questions
Don't answer Buildbase API specifics from memory — open the relevant file first. The "when to load" column tells you the moment each file becomes relevant.
| When to load | File |
|---|---|
| First when a developer reports a bug or something "doesn't work" | knowledge/failure-library/top-mistakes.md |
| First when a developer is starting a fresh integration | knowledge/learning/beginner-path.md |
| Before correcting a developer who seems confused about how the SDK behaves | knowledge/misconceptions/common-wrong-beliefs.md |
| When the question is "which feature/component do I use?" | knowledge/decision-trees/which-feature-to-use.md |
| When the question is a "X vs Y?" tradeoff | knowledge/decision-rules/when-to-use-what.md |
| "What is Buildbase?" / plain-language explanation for a beginner | knowledge/explain-buildbase-simply.md |
| When a term needs defining | knowledge/explain-buildbase-simply.md (plain), knowledge/mental-models/key-concepts.md, knowledge/glossary/terms.md |
| Implementing or debugging sign-in / session / cookies | knowledge/sdk/auth.md |
| Implementing workspace switching / multi-tenant | knowledge/sdk/workspace.md |
| Implementing subscriptions / plans / trials / pricing page | knowledge/sdk/billing.md |
| Implementing feature flags | knowledge/sdk/feature-flags.md |
| Implementing metered usage / quota recording | knowledge/sdk/quota-usage.md |
| Implementing prepaid credits | knowledge/sdk/credits.md |
| Implementing push / email notifications | knowledge/sdk/notifications.md |
| Any server-side work — API routes, background jobs, webhooks, Express | knowledge/sdk/server-side.md |
| Using Buildbase from a non-Node backend (Python, Go, Ruby, PHP, …) or raw HTTP | knowledge/http-api/using-from-any-language.md |
| Exact HTTP endpoints / methods / paths / payloads | knowledge/http-api/endpoints.md (+ overview.md) |
| Verifying inbound webhooks in any language | knowledge/http-api/webhooks.md |
| Writing the full Next.js wiring end-to-end | knowledge/patterns/nextjs-integration.md |
| Quick factual answer to a common question | knowledge/faq/frequently-asked.md |
---
Core mental models
Establish these before showing any code.
Org → Workspace → User. Everything — subscriptions, quotas, feature flags, credits — belongs to a workspace. Users join workspaces with roles. The org is the developer's product registered in the Buildbase dashboard.
Dashboard first, code second. Feature slugs, plan slugs, quota slugs, and notification event slugs must exist in the Buildbase dashboard before any SDK code referencing them will work. Code alone does nothing if the dashboard isn't configured.
Gates have three states, not two. Every When* component returns null (or loadingComponent) while loading, renders children when the condition is met, and returns null (or fallbackComponent) when not. "Gate shows nothing" almost always means loading state or missing dashboard config — not a bug.
Two tokens coexist. The Buildbase sessionId (httpOnly cookie) authenticates against the Buildbase platform. Any JWT the developer issues for their own API is separate. These are independent.
---
Security — flag these immediately, before anything else
If you see any of these, stop and correct them before continuing:
NEXT_PUBLIC_BUILDBASE_CLIENT_SECRET— exposes the secret to every browser visitor. Move toBUILDBASE_CLIENT_SECRET(server-side only,/api/auth/tokenendpoint only).sessionIdstored inlocalStorage— must be an httpOnly cookie, unreachable by JavaScript.- Protected API routes with no
auth()call at the top. - Webhook endpoint without
verifyWebhookSignature.
Read knowledge/failure-library/top-mistakes.md section "Security Vulnerabilities" for full detail.
---
First integration — the order matters
If a developer is setting up Buildbase for the first time, offer a choice before dumping everything — this directly serves less-experienced developers who get overwhelmed:
"I can either walk you through this milestone-by-milestone (sign-in first, then gates, then billing — confirming each works before moving on), or give you the full setup in one go. Which do you prefer?"
If they want guidance, follow knowledge/learning/beginner-path.md one milestone at a time and use its checkpoint questions to confirm understanding before advancing. If they want everything at once, give the full wiring from knowledge/patterns/nextjs-integration.md.
Either way, the order is not arbitrary:
0. Have a project. A Next.js App Router + TypeScript app. If they don't have one: npx create-next-app@latest my-app --typescript --app --src-dir --import-alias "@/*". This also sets up the @/ import alias the code relies on. Confirm the framework before pasting any code. 1. Credentials from the dashboard at console.buildbase.app (serverUrl, orgId, clientId, clientSecret, redirectUrl) — and in the dashboard's OAuth App, enable a login method and allow-list the redirectUrl, or sign-in fails 2. Install @buildbase/sdk (needs React 18 or 19 — the official starter uses React 19; node ≥ 18) 3. src/lib/buildbase.ts — BuildBase() factory reading from cookie 4. Three auth API routes — /api/auth/token, /api/auth/session, /api/auth/signout 5. src/components/saas-provider.tsx — 'use client' wrapper with SaaSOSProvider 6. Root layout — import '@buildbase/sdk/css' and wrap with provider 7. First gate — WhenAuthenticated protecting a page, then test sign-in end-to-end
Do not skip ahead. Developers who jump to billing before auth works will struggle. The complete, beginner-proof version of this with verification checks is knowledge/sdk/quick-start.md — prefer walking that.
---
Validation constraints
These throw at startup. Check these first if the app crashes immediately:
| Prop | Rule |
|---|---|
orgId | Exactly 24 hexadecimal characters — not an org name, not a slug |
version | Must be ApiVersion.V1 or the string 'v1' |
serverUrl | Valid URL with scheme (https:// or http://) |
---
When a developer seems stuck
Before suggesting code, check knowledge/misconceptions/common-wrong-beliefs.md. Most "bugs" are misconceptions. Identify the wrong belief first, correct the mental model, then show the fix. Correcting the model prevents the same mistake from recurring.
For runtime errors, read knowledge/failure-library/top-mistakes.md. It documents the symptom, root cause, detection method, and recovery steps for the 30 most common integration failures.
The #1 support question: "my gate renders nothing"
This is the single most common confusion. Walk it in this order before assuming a bug:
Gate (When*) renders nothing
│
├─ Is the user/workspace/subscription still loading?
│ → Gates return null while loading. Add loadingComponent to see it.
│ ✅ <WhenSubscription loadingComponent={<Spinner/>}>
│ ❌ assuming null === "condition not met"
│
├─ Does the referenced slug exist in the dashboard?
│ → Feature/plan/quota slugs must be created in the dashboard FIRST.
│ A correct slug that doesn't exist yet silently fails.
│
├─ Is the CSS imported at the root?
│ → import '@buildbase/sdk/css'; (missing → unstyled / invisible)
│
└─ Is this component inside <SaaSOSProvider>?
→ Gates outside the provider have no context and render nothing.---
What not to do
- Do not invent SDK behavior. If you are not certain something exists, say so and tell the developer to check the source or docs. (Only
INSUFFICIENT_CREDITSis a guaranteed error-code string; the SDK does not expose a fixed error-code enum — don't claim codes likeSESSION_EXPIREDexist.) - Do not generate code before establishing the mental model.
- Do not show advanced patterns to beginners — route to
knowledge/learning/beginner-path.mdinstead. - Do not show the same answer to a solo founder and an enterprise developer — read
knowledge/user-model/personas.mdand tailor. - Do not skip dashboard configuration warnings. Every slug-based feature requires dashboard setup first.
---
Reference Library
What each file contains, so you know whether it's worth opening:
SDK reference (knowledge/sdk/)
quick-start.md— the minimal end-to-end first integrationauth.md—useSaaSAuth, the three auth callbacks, events, redirect preservationworkspace.md—useSaaSWorkspaces,WorkspaceSwitcher, switch vs set, workspace modesbilling.md— subscription gates, trials,PricingPage, multi-currency utilitiesfeature-flags.md— workspace vs user features,useUserFeatures, programmatic checksquota-usage.md—useRecordUsage, batch recording, response shape, quota gatescredits.md—useConsumeCredits,CreditActionsProvider, public packages,INSUFFICIENT_CREDITSnotifications.md— push service-worker setup,notification.send, channels, merge tagsserver-side.md—BuildBase()factory, all action modules, webhook verification (options-object API)
Plain-language onboarding
explain-buildbase-simply.md— jargon-free explanation + analogies for true beginners ("what is this?")sdk/quick-start.md— the golden path: zero → signed in, every file shown, ✅ check after each step
Learning & user model
learning/beginner-path.md— milestone-by-milestone path (0→4) with checkpoint questionsuser-model/personas.md— 6 developer archetypes and their distinct needsuser-model/experience-stages.md— Explorer→Power User; what each knows and needs next
Diagnosis
failure-library/top-mistakes.md— 30 mistakes: symptom, cause, detection, recoverymisconceptions/common-wrong-beliefs.md— 20 wrong beliefs with correctionstroubleshooting/common-errors.md— runtime errors and fixes
Decisions
decision-trees/which-feature-to-use.md— "what do I use?" treesdecision-rules/when-to-use-what.md— "X vs Y" tradeoffs
HTTP API (any language / non-Node backends)
http-api/overview.md— base URL, thex-session-idauth header, envelope/error rules, what's not pure-HTTPhttp-api/endpoints.md— full endpoint catalog (method, path, body, response) for every SDK callhttp-api/webhooks.md— HMAC-SHA256 webhook verification recipe with Python/Go codehttp-api/using-from-any-language.md— login/code-exchange flow + Python/Go examples
Patterns & quick lookup
patterns/nextjs-integration.md— complete production Next.js wiring (all 7 files)faq/frequently-asked.md— common questions with direct answersglossary/terms.md— term definitionsmental-models/key-concepts.md— the 5 core mental models in depth
---
Keywords: Buildbase, @buildbase/sdk, @buildbase/sdk/react, SaaSOSProvider, BuildBase, useSaaSAuth, useSaaSWorkspaces, useSubscriptionContext, useRecordUsage, useConsumeCredits, WhenAuthenticated, WhenSubscription, WhenSubscriptionToPlans, WhenQuotaAvailable, WhenCreditsAvailable, WhenWorkspaceFeatureEnabled, WhenWorkspaceRoles, WorkspaceSwitcher, PricingPage, bb-session-id, orgId, clientSecret, workspace, tenant, subscription, plan, trial, feature flag, quota, usage, credits, notification, webhook, multi-tenant SaaS, auth provider, billing integration.
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "buildbase",
"displayName": "Buildbase SDK Integration",
"version": "0.1.0",
"description": "Expert guide for integrating the Buildbase SDK (@buildbase/sdk) into any application — auth, workspaces, billing, feature flags, quota, credits, notifications, server-side usage, webhooks, and using Buildbase from any backend language via raw HTTP.",
"author": {
"name": "Buildbase",
"url": "https://github.com/buildbase-app"
},
"homepage": "https://github.com/buildbase-app/claude-skill",
"repository": "https://github.com/buildbase-app/claude-skill",
"license": "MIT",
"keywords": [
"buildbase",
"sdk",
"saas",
"authentication",
"billing",
"stripe",
"feature-flags",
"quota",
"credits",
"notifications",
"nextjs",
"multi-tenant"
]
}
Decision Rules: When to Use What
Contents
- Gate Components vs Hooks — declarative UI vs logic
- Client-Side vs Server-Side Usage Recording — where to record usage
- Credits vs Quotas — prepaid units vs resetting limits
- Personal Mode vs Platform Mode — solo vs multi-user apps
- switchToWorkspace vs setCurrentWorkspace — change vs restore workspace
- Workspace Features vs User Features — per-team vs per-individual
- onWorkspaceChange vs handleEvent('workspace:changed') — before vs after load
- next-auth Pattern vs Custom Auth — running alongside next-auth
- When to Use withSession() vs getSessionId — per-request vs cookie session
- RBAC: WhenRoles vs WhenWorkspaceRoles — global vs workspace roles
Gate Components vs Hooks
Use gate components (WhenSubscription, WhenQuotaAvailable, etc.) when:
- Conditionally rendering UI elements
- You just need "show or hide" behavior
- You want declarative, readable JSX
Use hooks (useSubscriptionContext, useQuotaUsageContext, etc.) when:
- You need subscription/quota data (not just the condition)
- You need to trigger a refetch
- You're performing logic in event handlers or async functions
- You need to check multiple conditions
---
Client-Side vs Server-Side Usage Recording
| Situation | Use |
|---|---|
| User initiates an action in the UI | useRecordUsage (client) |
| API route processes a request | usage.record (server) |
| Background job or cron | usage.record (server) |
| Webhook handler | usage.record (server) |
| File upload (server-processed) | usage.record (server) |
---
Credits vs Quotas
| Scenario | Use |
|---|---|
| Fixed monthly allocation included in plan | Quota |
| Prepaid units purchased separately | Credits |
| Resets every billing period | Quota |
| Never resets (spend down until refilled) | Credits |
| AI token budget | Credits |
| API call limit | Quota |
| Storage limit | Quota |
---
Personal Mode vs Platform Mode
| App type | Mode |
|---|---|
| Solo productivity tool, personal notes, individual subscriptions | Personal Mode |
| Team collaboration, B2B SaaS, multi-user accounts | Platform Mode |
| E-commerce, per-user accounts but with shared resources | Platform Mode |
Configured in Buildbase dashboard — no code changes needed.
---
switchToWorkspace vs setCurrentWorkspace
| Function | Use when |
|---|---|
switchToWorkspace(workspace) | User clicks "Switch to" — runs onWorkspaceChange first (pass the workspace object, not an id) |
setCurrentWorkspace(workspace) | Restoring saved state on page load — bypasses callback |
---
Workspace Features vs User Features
| Feature type | Use when |
|---|---|
| Workspace feature flag | Feature is per-team/plan (e.g., "team has analytics") |
| User feature flag | Feature is per-individual (e.g., "user is in beta") |
---
onWorkspaceChange vs handleEvent('workspace:changed')
| Callback | Use for |
|---|---|
onWorkspaceChange | Work that MUST complete before workspace loads (token generation) |
handleEvent('workspace:changed') | Work that happens AFTER workspace loads (analytics, app state sync) |
---
next-auth Pattern vs Custom Auth
The Buildbase SDK uses the same session pattern as next-auth:
- httpOnly cookie stores session token
- Server endpoint reads cookie and returns token
- Client-side callback calls server endpoint
If you're already using next-auth, Buildbase runs alongside it — they use different cookie names and manage different auth states. Buildbase handles the SaaS platform layer; next-auth can handle your own user auth.
---
When to Use withSession() vs getSessionId
| Scenario | Use |
|---|---|
| Next.js API routes (async request context available) | getSessionId callback (reads from cookie) |
| Express / Hono / Fastify | withSession(req.headers['x-session-id']) |
| Background jobs / service accounts | withSession(process.env.SERVICE_SESSION_ID) |
| Webhook handlers (no user context) | withSession(serviceToken) |
---
RBAC: WhenRoles vs WhenWorkspaceRoles
| Component | Use when |
|---|---|
WhenRoles roles={['admin']} | Checking global user role (super-admin use case) |
WhenWorkspaceRoles roles={['owner', 'admin']} | Checking role within the current workspace |
In most SaaS apps, WhenWorkspaceRoles is what you want — users have different roles in different workspaces.
Decision Trees: Which Feature to Use?
Four decision trees for the most common "what should I use?" questions. Read them like a conversation — they're meant to guide you through the reasoning, not just give you an answer.
Contents
- Tree 1: I Want to Restrict Content — pick the right gate
- Tree 2: Record a User Action — Client or Server — where to record usage
- Tree 3: Credits vs Quotas — choose the billing model
- Tree 4: Personal vs Platform Mode — choose the workspace mode
---
Tree 1: I Want to Restrict Content — What Do I Use?
You want to show or hide something based on the user's state. Start here and follow the branches.
Ask yourself: Is the user signed in?
- If the question is "is this user authenticated at all?" → use auth gates
- Show only to signed-in users:
<WhenAuthenticated> - Show only to signed-out users:
<WhenUnauthenticated> - Check programmatically:
useSaaSAuth().isAuthenticated - Stop here. You don't need billing or feature gates for a basic auth check.
- If the user is signed in, continue.
Ask yourself: Does access depend on what they've paid for?
- If yes, continue to billing.
- If no, skip to feature flags.
Billing: Does it depend on whether they have any active subscription?
- Yes, show only to subscribers:
<WhenSubscription> - Yes, show only to non-subscribers (free users):
<WhenNoSubscription> - No, you want a specific plan. Continue.
Billing: Does it depend on which specific plan they're on?
- Yes, one or more plans:
<WhenSubscriptionToPlans plans={['pro', 'enterprise']}> - Yes, you want to exclude a specific plan: use
useSubscriptionContext()and checkresponse?.plan?.slug !== 'free'directly - No, continue.
Billing: Does it depend on trial state?
- User is in a trial:
<WhenTrialing> - Trial is ending soon:
<WhenTrialEnding daysThreshold={7}> - Trial has ended (no longer trialing and no active plan): combine
<WhenNotTrialing>with<WhenNoSubscription>, or checkuseTrialStatus(). (There is noWhenTrialEndedcomponent.) - Continue if not trial-related.
Ask yourself: Does access depend on a specific feature being enabled for the workspace?
- This is about workspace-level feature flags (set per workspace or per plan):
- Gate content:
<WhenWorkspaceFeatureEnabled slug="your-feature"> - Check programmatically:
useUserFeatures().isFeatureEnabled('your-feature'), or read the workspace's feature map directly —useSaaSWorkspaces().currentWorkspace?.features?.['your-feature'](featuresis aRecord<string, boolean>, so index it; it has no.includes()) - Remember: the feature must exist in the dashboard AND be on the workspace's plan first.
Ask yourself: Does access depend on individual user-level features?
- This is for features toggled per-user (beta access, personal experiments):
<WhenUserFeatureEnabled slug="beta-access">useUserFeatures().isFeatureEnabled('beta-access')
Ask yourself: Does access depend on how much the user has used?
- This is quota-based: they have a monthly limit and might have used it up.
- They have remaining quota:
<WhenQuotaAvailable slug="api_calls"> - They're approaching the limit:
<WhenQuotaThreshold slug="api_calls" threshold={80}>(80% consumed) - They're at the limit:
<WhenQuotaExhausted slug="api_calls">
Ask yourself: Does access require prepaid credits?
- This is for consumable units users purchase (AI tokens, generation credits):
- They have enough credits:
<WhenCreditsAvailable min={5}> - They've run out:
<WhenCreditsExhausted>
Ask yourself: Does access depend on their role within the workspace?
- This is role-based access control (RBAC):
- Global user roles (admin/user across the platform):
<WhenRoles roles={['admin']}> - Workspace-specific roles (developer-defined strings, commonly owner/admin/member):
<WhenWorkspaceRoles roles={['owner', 'admin']}> - Prefer workspace roles for most access control — they're more granular.
Ask yourself: Does access depend on a custom permission?
- You've defined custom permissions in
defaultPermissionsonSaaSOSProvider: - Use the permission check component or
usePermissions().can('reports:export')(the hook returns{ can, permissions, isOwner, role }— the method iscan)
---
Tree 2: I Want to Record That a User Did Something — Client or Server?
You're recording usage, tracking an event, or consuming credits. Should that happen in the browser or in your API?
Ask yourself: Where does the action actually complete?
- If the action is purely a client-side event (page view, UI interaction, a toggle the user clicks):
- Consider
useRecordUsagefrom the React SDK - But continue reading — there are important caveats.
- If the action requires a server round-trip (API call, file processing, database write):
- Record server-side. Don't record client-side. Continue.
Ask yourself: Could a malicious user trigger the action without triggering the recording?
- If yes (the recording is in a React
onClickbut the API accepts calls directly): - Must be server-side. Move recording to the API route.
- If no (the only way to trigger the action is through your API):
- Server-side is still preferred, but client-side is acceptable for low-stakes tracking.
Ask yourself: Could this be called twice for the same action? (Network retry, user double-click, job retry)
- If yes:
- Must use
idempotencyKeyto prevent double-counting. - Generate a unique key per action attempt (UUID, request ID).
- This applies whether recording client-side or server-side.
usage.record(workspaceId, { quotaSlug, quantity, idempotencyKey: uuid() })
Ask yourself: Is this inside a background job or cron task?
- Always server-side. Never client-side.
- Use the
BuildBase()factory directly. - Use
withSession(serviceSessionId)if you need a user-scoped session for a job. - Use
recordBatch(workspaceId, { items: [...] })for bulk operations (max 100 items per call).
The short answer:
- Client
useRecordUsage→ page views, analytics events, low-stakes UI interactions - Server
usage.record(...)→ anything that affects billing, anything the user could manipulate, anything in a background job
---
Tree 3: Credits vs Quotas — Which Do I Use?
Both involve "counting" how much of something a workspace uses. The billing model is different.
Ask yourself: Does usage reset automatically at the end of the billing period?
- Yes, it resets → Quota
- Example: 1000 API calls per month. When the billing period ends, the count resets.
- Plan includes 1000 calls → workspace uses 750 → next month starts at 0 again.
- Configure as a quota on the plan in the dashboard.
- No, it doesn't reset automatically → Credits
- Example: 1000 AI tokens purchased. The user buys them, spends them down.
- They don't get more at the end of the month unless they buy more.
- Configure as credit packages in the dashboard.
Ask yourself: Do users purchase this upfront, in advance?
- Yes, they buy a pack of N units → Credits
- Users go to a credits purchase flow to buy more.
- Use
CreditActionsProviderfor the buy-and-consume UI. - Use
WhenCreditsAvailable min={N}to gate actions that cost credits.
- No, it's included in their subscription plan → Quota
- The plan says "includes 1000 API calls/month."
- Overage can be configured per-unit if you want pay-as-you-go above the limit.
Ask yourself: Does it have overage billing?
- Yes, users can go over the limit and pay per additional unit → Quota with overage
- Configure overage pricing on the plan's quota definition in the dashboard.
- SDK handles the
availablevsoveragestate automatically.
- No, when they run out they're blocked → Quota without overage or Credits
- For time-reset blocking limits: Quota without overage.
- For prepaid spending: Credits.
Common mapping:
- Monthly API call limit → Quota
- Monthly email sends → Quota
- Monthly storage GBs → Quota
- AI generation tokens → Credits
- One-time compute units → Credits
- "Token packages" users buy → Credits
- Per-generation cost → Credits (consume N credits per generation)
The rule of thumb: Credits are like a prepaid card — spend until empty, buy more. Quotas are like a data plan — use what's included, potentially pay for more, resets monthly.
---
Tree 4: Personal Mode vs Platform Mode — Which Do I Configure?
Workspace mode is set in the Buildbase dashboard and determines how users and workspaces relate.
Ask yourself: Is each user their own independent customer?
- Yes, each user signs up for themselves, has their own subscription, and doesn't share anything with other users:
- Personal mode
- Example: a solo tool, a personal productivity app, a developer API service
- Each user signs up → one workspace is automatically created for them → they subscribe → done
- Users never create workspaces manually or invite members
- No, users belong to teams or organizations that share resources:
- Platform mode
- Example: Slack, Notion, GitHub, Vercel — any B2B SaaS
- Organizations subscribe, not individuals
- One org can have multiple workspaces, multiple members per workspace
Ask yourself: Is this a B2C product (selling to individuals)?
- Yes → Personal mode
- Users don't think of themselves as "workspace owners"
- The workspace concept is invisible to them
- Their account IS their workspace
- No, it's B2B (selling to companies) → Platform mode
- Users join a company account
- Companies have multiple team members
- Billing is per-organization, not per-user
Ask yourself: Do users need to invite other people to their account?
- Yes → Platform mode
- Platform mode enables workspace membership, invitations, and per-workspace roles
- Personal mode doesn't support multi-user workspaces
- No, it's a single-user product → Personal mode
Ask yourself: Can one user have multiple "projects" or "accounts" within your product?
- Yes, users can have multiple separate contexts (like multiple GitHub organizations) → Platform mode
- Platform mode supports multiple workspaces per user
- Users can switch between workspaces
- No, one account per user → Personal mode
The quick test:
"If two employees at the same company both use my product, should they be on the same account or different accounts?"
- Same account, shared subscription → Platform mode
- Different accounts, separate subscriptions → Personal mode
What changes between the modes in the SDK:
- Personal mode:
autoCreateFirstWorkspace: truein dashboard. Users never see workspace management UI. - Platform mode: Users see workspace creation, invitations, and the workspace switcher.
- Both modes: same SDK hooks, same gate components, same auth flow. The mode changes the user experience, not the technical integration.
Buildbase Explained Simply
Plain-language explanations for someone brand new. No jargon. Use these analogies when a beginner asks "what is this?" or "why do I need it?"
---
What is Buildbase?
Imagine you want to open a shop. Before you can sell anything, you'd normally have to build:
- a lock and a membership desk (so only the right people get in) → that's authentication
- a cash register that handles cards, subscriptions, and receipts → that's billing
- a rule book of who's allowed to do what → that's permissions and feature flags
- a meter that tracks how much each customer uses → that's quotas and usage
Building all of that yourself takes months and is easy to get wrong (especially the security and payments parts).
Buildbase is a company you hire to run all of that for you. You connect your app to Buildbase, and it handles the locks, the register, the rule book, and the meter. You just build the actual thing your customers came for.
---
The words you'll keep seeing (in plain English)
| Word | What it really means |
|---|---|
| SDK | The toolbox (@buildbase/sdk) you install to talk to Buildbase. "SDK" = "Software Development Kit" = a bundle of ready-made code. |
| Org (organization) | Your company/product as Buildbase knows it. You get one when you sign up. |
| Workspace | A single customer account or team. If you build a team tool, each team is one workspace. Billing and limits attach to a workspace. |
| User | A person who logs in. A user can belong to one or more workspaces. |
| Session | Proof that a user is logged in right now. Stored as a sessionId. |
| Cookie | A small note the browser keeps. We store the login proof in a special httpOnly cookie — one that page scripts can't read, so it can't be stolen. |
| Provider | One React component (SaaSOSProvider) you wrap your whole app in. It makes Buildbase available everywhere. You write one; the SDK does the rest. |
| Factory | A function (BuildBase(...)) you call once on your server that hands back a set of tools. |
| Gate | A component whose name starts with When… (like WhenAuthenticated). It shows its contents only when a condition is true. |
| Slug | A short lowercase id for something you set up in the dashboard, like pro for a plan or analytics for a feature. Your code refers to things by their slug. |
| Dashboard | The Buildbase website (console.buildbase.app) where you (the developer) configure plans, features, and login methods. Not the same as your app. Official docs: docs.buildbase.app. |
| Plan | A pricing tier (Free, Pro, Enterprise). You define these in the dashboard; Buildbase charges cards via Stripe. |
| Quota | A usage limit that resets each billing period (e.g. "5,000 API calls/month"). |
| Credits | Prepaid units a customer buys up front (e.g. "100 AI generations"). They don't reset. |
---
The one rule that prevents most confusion
Dashboard first, then code.
Many Buildbase features depend on something you set up in the dashboard before your code can use it. If you write <WhenWorkspaceFeatureEnabled slug="analytics"> but never created an "analytics" feature in the dashboard, nothing shows up — and there's no error, just silence. That silence confuses everyone.
So whenever a feature involves a slug (a plan, a feature, a quota), the order is always: 1. Create it in the dashboard (give it a slug). 2. Reference that exact slug in your code.
---
The smallest possible explanation of how login works
1. Your user clicks Sign In. 2. Buildbase shows them a login page and, when they succeed, sends them back to your app with a temporary code. 3. Your app's server quietly trades that code (using a secret key) for a session, and remembers it in a cookie. 4. The user is now logged in, and stays logged in across refreshes.
You don't have to build the login page or handle passwords — Buildbase does. You just wire up the hand-off. The step-by-step is in sdk/quick-start.md.
---
What Buildbase does NOT do
So you don't go looking for features that aren't there:
- It's not your database. Your app's own data (todos, posts, whatever) lives in your own database. Buildbase handles accounts, billing, and limits — not your business data.
- It's not a UI framework. It gives you some ready-made screens (login, pricing, settings), but you build your actual app's interface.
- It doesn't replace your backend. You still have your own API routes; Buildbase plugs into them.
Top 30 Integration Mistakes
Organized from most common to most dangerous. Most bugs are in the first two categories.
Contents
- Setup & Configuration (1–8) — orgId format, clientSecret in NEXT_PUBLIC_, missing CSS import, wrong version string, nested provider, missing auth endpoints, cookie name mismatch, env vars not loaded in production
- Authentication (9–14) — handleAuthentication return value, getSession undefined vs null, cookie not httpOnly, wrong token URL, signout not clearing cookie, silent code-exchange failure
- Workspace & Billing (15–19) — setCurrentWorkspace vs switchToWorkspace, checking features before workspace loads, onWorkspaceChange JWT, workspace mode mismatch, subscription not immediate after checkout
- Feature Flags & Quota (20–24) — feature slug missing in dashboard, feature not on plan, quota slug undefined, client-side recording of server-validated actions, missing idempotencyKey
- Security Vulnerabilities (25–28) — clientSecret exposed to browser, sessionId in localStorage, no auth check in API routes, webhook not signature-verified
- Performance & Architecture (29–30) — BuildBase() called per request, excessive re-renders from subscription context
---
Setup & Configuration (Mistakes 1–8)
Mistake 1: Wrong orgId Format
What the developer did: Set orgId="mycompany" or orgId="my-app-name" — a human-readable name.
Symptom: App crashes on load: Error: Invalid orgId. Must be a 24-character hexadecimal string.
Why it happens: The prop name sounds like an identifier you'd name yourself. The 24-hex-char constraint isn't obvious from the prop name alone.
Detection: Check process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID?.length. Must be exactly 24. Log JSON.stringify(orgId) to check for hidden whitespace or quotes.
Recovery: Open Buildbase dashboard → Settings → General → Organization ID. Copy the value exactly (it looks like 64a3f5b2c1d4e7890abc1234). Paste into env var.
---
Mistake 2: clientSecret in NEXT_PUBLIC_ Env Var
What the developer did: Set NEXT_PUBLIC_BUILDBASE_CLIENT_SECRET=sk_... in .env.local.
Symptom: App works normally, but the secret is embedded in the JavaScript bundle and visible to anyone who opens browser DevTools → Sources.
Why it happens: All SDK env vars are seen together; developers apply the same NEXT_PUBLIC_ prefix pattern to all of them.
Detection: Run grep -r "NEXT_PUBLIC_BUILDBASE_CLIENT_SECRET" . in your project. Check .env.local and .env.production.
Recovery: Rename to BUILDBASE_CLIENT_SECRET (no NEXT_PUBLIC_ prefix). Verify it's used only in server-side code (/api/auth/token). Rotate the secret in the Buildbase dashboard if it was already deployed.
---
Mistake 3: Missing CSS Import
What the developer did: Installed and configured the SDK but didn't add import '@buildbase/sdk/css' to the root layout.
Symptom: SDK UI components (auth modal, workspace settings dialog, pricing page) render with broken or missing styles. Looks like a layout crash.
Why it happens: Most npm packages auto-include their styles. Explicit CSS imports are less common in modern tooling.
Detection: Search for buildbase/css in your codebase: grep -r "buildbase/css" .. If no results, the import is missing.
Recovery: Add import '@buildbase/sdk/css' to app/layout.tsx (Next.js App Router) or pages/_app.tsx (Pages Router). Must be at the top level, not inside a 'use client' component.
---
Mistake 4: Wrong Version String
What the developer did: Passed version="v2", version="1", or omitted the version prop on SaaSOSProvider.
Symptom: App throws a validation error on load about an invalid version.
Why it happens: The version requirement feels like boilerplate. Developers guess the format.
Detection: Check the version prop value on SaaSOSProvider. Only 'v1' or ApiVersion.V1 (imported from @buildbase/sdk) are valid.
Recovery: Use version={ApiVersion.V1} (import ApiVersion from @buildbase/sdk) or version="v1". The ApiVersion enum is preferred as it's refactor-safe.
---
Mistake 5: SaaSOSProvider Nested Inside Another SaaSOSProvider
What the developer did: Added a second SaaSOSProvider in a child component (e.g., inside a dashboard layout) with different config.
Symptom: Undefined behavior — context values may conflict, workspace state may not update correctly, subscription data may be stale or wrong.
Why it happens: React context providers can be nested for different values. Developers try to apply workspace-specific config by nesting.
Detection: Search your codebase for SaaSOSProvider: grep -r "SaaSOSProvider" src. More than one instance is a bug.
Recovery: Remove all but the outermost SaaSOSProvider. Use onWorkspaceChange callback to handle workspace-specific behavior. Use feature flags and permissions for workspace-level customization.
---
Mistake 6: Missing Auth Endpoints
What the developer did: Added SaaSOSProvider with getSession, handleAuthentication, and onSignOut callbacks that call /api/auth/session, /api/auth/token, and /api/auth/signout — but never created those route files.
Symptom: Clicking "Sign In" redirects to Buildbase OAuth, returns with a ?code= in the URL, but the user never gets signed in. isAuthenticated stays false.
Why it happens: The provider callbacks look like configuration, not implementation. Developers assume the SDK creates the routes.
Detection: Make a GET request to /api/auth/session in the browser. If you get a 404, the route doesn't exist.
Recovery: Create three route handlers:
app/api/auth/token/route.ts— POST, exchanges code, sets cookieapp/api/auth/session/route.ts— GET, reads cookie, returns sessionIdapp/api/auth/signout/route.ts— POST, clears cookie
See knowledge/patterns/nextjs-integration.md for full implementations.
---
Mistake 7: Wrong Cookie Name Between Routes and Factory
What the developer did: Sets the cookie as session-id in /api/auth/token but reads it as bb-session-id in /api/auth/session (or vice versa). Or the factory's getSessionId reads a different cookie name than what's set.
Symptom: Session is set after login but not read back. getSession returns null on page refresh. User is logged out on every page reload.
Why it happens: The cookie name is a magic string that must be consistent across three places. Developers set it in one place and forget to update the others.
Detection: In browser DevTools → Application → Cookies, check what cookie name is actually set after login. Compare to what /api/auth/session reads and what the factory's getSessionId reads.
Recovery: Define the cookie name as a single constant: export const SESSION_COOKIE_NAME = 'bb-session-id' in lib/buildbase.ts. Import and use it in all three auth routes and the factory.
---
Mistake 8: Env Vars Not Loaded in Production
What the developer did: Set env vars in .env.local which is not committed or deployed. Production build has missing env vars.
Symptom: App crashes in production with Invalid orgId, Invalid serverUrl, or Cannot read properties of undefined errors. Works fine in local dev.
Why it happens: .env.local is gitignored by default. Developers forget to set the same vars in their hosting provider (Vercel, Railway, Fly.io).
Detection: Check the hosting provider's environment variables dashboard. Log process.env.NEXT_PUBLIC_BUILDBASE_ORG_ID in a server route — if undefined, the var isn't set.
Recovery: Add all required env vars to your hosting provider's environment variables section. For Vercel: Settings → Environment Variables. Remember: NEXT_PUBLIC_ vars must also be added there (not just server-side vars).
---
Authentication (Mistakes 9–14)
Mistake 9: handleAuthentication Not Returning sessionId
What the developer did: Implemented handleAuthentication but returned undefined, {}, or { token: data.sessionId } instead of { sessionId: data.sessionId }.
Symptom: Auth flow appears to complete (no error, URL code is processed) but isAuthenticated remains false and the session doesn't persist.
Why it happens: The return type requirement ({ sessionId: string }) isn't enforced at the TypeScript level in all setups. Silent failure.
Detection: Add a console.log inside handleAuthentication to log the return value before returning. Check that it's { sessionId: "..." } with a non-null string.
Recovery: The function must return { sessionId: string } where sessionId is the actual Buildbase session token. Make sure /api/auth/token returns { sessionId: data.sessionId } and that handleAuthentication returns the result correctly.
---
Mistake 10: getSession Returning Undefined Instead of Null
What the developer did: getSession callback returns undefined when there's no session, instead of null.
Symptom: SDK may behave unexpectedly. Some internal checks distinguish null (no session) from undefined (error in callback). Auth state may not initialize correctly.
Why it happens: JavaScript developers often treat undefined and null as equivalent. They're not in this context.
Detection: Add logging to getSession: console.log('session:', sessionId). If it logs undefined when no user is logged in, it's wrong.
Recovery: Return null explicitly: return data.sessionId ?? null. The nullish coalescing operator ensures undefined from missing cookie becomes null.
---
Mistake 11: Session Cookie Not HttpOnly
What the developer did: Set the session cookie without the HttpOnly flag, making it accessible via document.cookie.
Symptom: No functional symptom — but the session is now vulnerable to XSS theft. Any injected script can steal the session token.
Why it happens: Developers copy a cookie-setting snippet that omits the security flags. The app "works" so the mistake isn't caught.
Detection: In browser DevTools → Application → Cookies → localhost. Check if the bb-session-id cookie has the HttpOnly checkbox checked. If not, it's missing the flag.
Recovery: In your /api/auth/token route, use response.cookies.set() with { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' }. Never use document.cookie to set session cookies.
---
Mistake 12: Token Endpoint Hitting Wrong Buildbase URL
What the developer did: In /api/auth/token, called ${serverUrl}/api/v1/auth/oauth2-token or ${serverUrl}/auth/token instead of ${serverUrl}/api/v1/auth/token.
Symptom: The token exchange returns a 404 or an error. Auth fails. The developer sees a 401 or 404 in the server logs.
Why it happens: The exact URL path isn't prominently documented. Developers guess from the pattern.
Detection: Check the fetch call in /api/auth/token. Log the full URL being called and the response status.
Recovery: The correct endpoint is ${process.env.NEXT_PUBLIC_BUILDBASE_SERVER_URL}/api/v1/auth/token. Body must include code, clientId, clientSecret, and orgId.
---
Mistake 13: Signout Not Clearing the Cookie
What the developer did: The POST /api/auth/signout route returns { success: true } but doesn't set Max-Age: 0 on the cookie.
Symptom: Clicking "Sign Out" calls the endpoint (200 response), but the user remains signed in. Refreshing the page shows them as still authenticated.
Why it happens: Developers implement the "sign out" response but forget that clearing a cookie requires setting it again with maxAge: 0.
Detection: After clicking sign out, check browser DevTools → Application → Cookies. If bb-session-id still exists, the signout isn't clearing it.
Recovery: In the signout route, use response.cookies.set(SESSION_COOKIE_NAME, '', { httpOnly: true, maxAge: 0, path: '/' }). The maxAge: 0 tells the browser to delete the cookie.
---
Mistake 14: Code Exchange Failing Silently
What the developer did: The /api/auth/token route fails (wrong secret, network error) but returns a 200 response anyway, so the SDK doesn't know auth failed.
Symptom: Auth appears to "complete" — the code is processed, no error is shown — but the user isn't actually signed in. isAuthenticated stays false.
Why it happens: The route catches errors but returns { success: false } with a 200 status instead of a 4xx. The SDK can't distinguish success from failure.
Detection: Add logging to the token route. Check what the Buildbase server returns when the code exchange fails. Ensure non-200 responses are returned on failure.
Recovery: Return NextResponse.json({ error: 'Auth failed' }, { status: 401 }) when the exchange fails. The SDK will surface this as an auth error rather than silently doing nothing.
---
Workspace & Billing (Mistakes 15–19)
Mistake 15: setCurrentWorkspace Instead of switchToWorkspace
What the developer did: When the user clicks "Switch to Workspace", called setCurrentWorkspace(workspace) instead of switchToWorkspace(workspace).
Symptom: The workspace visually changes in the UI, but onWorkspaceChange callback doesn't fire. The internal JWT for the developer's own API isn't refreshed. API calls use a stale workspace token.
Why it happens: setCurrentWorkspace sounds like the right function — "set the current workspace." The distinction between the two functions isn't obvious from the names.
Detection: Add logging to onWorkspaceChange callback. If switching workspaces doesn't trigger it, switchToWorkspace isn't being used.
Recovery: Use switchToWorkspace(workspace) (pass the workspace object, not an id) for user-initiated switches. It triggers all callbacks and loading states. Use setCurrentWorkspace(workspace) only for programmatic initialization where you don't want callback side effects.
---
Mistake 16: Checking Workspace Features Before Workspace Loads
What the developer did: Checks isFeatureEnabled('analytics') or renders WhenWorkspaceFeatureEnabled before currentWorkspace is set.
Symptom: Feature flag always returns false on first load. After workspace selection, it works correctly.
Why it happens: Workspace data loads asynchronously. Feature checks before workspace load get the pre-load state (no features enabled).
Detection: Log currentWorkspace in the component where the feature check fails. If it's null when the check runs, that's the cause.
Recovery: Guard feature checks behind workspace loading: if (!currentWorkspace) return null. Or use WhenWorkspaceFeatureEnabled which handles loading state internally.
---
Mistake 17: onWorkspaceChange Not Generating Internal JWT
What the developer did: Implemented workspace switching but didn't use onWorkspaceChange to generate a workspace-scoped JWT for their own API.
Symptom: After switching workspaces, API calls to the developer's own backend use the wrong workspace context. Data from the previous workspace appears.
Why it happens: The onWorkspaceChange callback isn't required for Buildbase API calls (the SDK handles session automatically). Developers don't realize their own API also needs to be informed about the workspace switch.
Detection: After switching workspaces, make an API call to your backend. Check what workspace it operates on.
Recovery: In onWorkspaceChange, call your /api/auth/workspace-token endpoint with the new workspace ID. Store the returned token (e.g., in localStorage or a React context). Use this token in Authorization headers for your own API calls.
---
Mistake 18: Workspace Mode Mismatch
What the developer did: Configured Buildbase in Platform mode (multi-tenant) but is building a B2C app where each user has exactly one account. Or vice versa.
Symptom: In Platform mode for B2C: workspace creation UI appears for every user, confusing them. Each user has to "set up a workspace" to use the app. In Personal mode for B2B: users can't invite team members or create multiple workspaces.
Why it happens: The mode is set in the Buildbase dashboard and feels like a technical detail. Developers choose without understanding the implications.
Detection: The workspace setup step in the user onboarding flow reveals the issue. If users see workspace creation prompts when they shouldn't, or can't invite members when they should, the mode is wrong.
Recovery: Change workspace mode in Buildbase dashboard → Settings → Workspace. Personal mode = one user, one auto-created workspace (B2C). Platform mode = multi-user, multi-workspace (B2B).
---
Mistake 19: Assuming Subscription Data Is Immediately Available After Checkout
What the developer did: After a user completes checkout, immediately checks subscription.get(workspaceId) server-side and expects the new subscription to be present.
Symptom: Immediately after checkout, subscription shows as inactive or on the free plan. After a few seconds or a page refresh, it updates correctly.
Why it happens: Developers expect checkout to be synchronous. Stripe webhook processing adds latency.
Detection: The subscription state is correct after 2-5 seconds but wrong immediately after the Stripe redirect.
Recovery: Don't check subscription state immediately after checkout redirect. Either: (1) poll subscription.get(workspaceId) with a short delay, (2) handle the server-side webhook (subscription.created/subscription.updated) to update your own records, or (3) show a "processing..." state for a few seconds before redirecting to a confirmed state. (Note: there is no client subscription:changed event via handleEvent — use webhooks or polling.)
---
Feature Flags & Quota (Mistakes 20–24)
Mistake 20: Feature Slug Doesn't Exist in Dashboard
What the developer did: Used <WhenWorkspaceFeatureEnabled slug="dark-mode"> in code but never created a "dark-mode" feature in the Buildbase dashboard.
Symptom: The gate renders nothing — silently. No error. Looks like the feature is disabled for all users.
Why it happens: The code-first mental model. The slug feels like a string you just make up. The dashboard step feels optional.
Detection: Open Buildbase dashboard → Features. Check if "dark-mode" exists. If not, that's the issue.
Recovery: Create the feature in the dashboard first. Name it, set the slug to "dark-mode". Enable it on the relevant plans. Then the code gate will work.
---
Mistake 21: Feature Exists But Isn't on the Plan
What the developer did: Created the feature in the dashboard but didn't add it to any plan.
Symptom: Same as above — gate shows nothing for all users even though the feature exists.
Why it happens: Creating a feature and assigning it to a plan are two separate steps. Developers do the first but forget the second.
Detection: Dashboard → Features → click "dark-mode" → check which plans include it. If none, that's the issue.
Recovery: In the Buildbase dashboard, edit each plan that should include the feature. Add the feature to the plan's feature list.
---
Mistake 22: Quota Slug Not Defined on the Plan
What the developer did: Used <WhenQuotaAvailable slug="api_calls"> but didn't define the "api_calls" quota on the workspace's plan.
Symptom: WhenQuotaAvailable shows the fallbackComponent for all users, even on paid plans. Or useQuotaUsageStatus returns unexpected values.
Why it happens: Same pattern as features — quota slugs must be defined on plans in the dashboard before SDK references to them work.
Detection: Dashboard → Plans → [plan name] → Quotas. Check if "api_calls" is listed.
Recovery: Add the quota to the plan in the dashboard: define the slug, included amount, overage pricing if applicable.
---
Mistake 23: Recording Usage Client-Side for Server-Validated Actions
What the developer did: Used useRecordUsage in a React component to record API call usage when a button is clicked.
Symptom: Users can bypass quota limits by manipulating the request or calling the endpoint without triggering the React component. Quota data becomes inaccurate.
Why it happens: useRecordUsage is accessible and easy to use from React. Developers don't think about whether the recording can be manipulated.
Detection: Can a user trigger the actual action (e.g., the API call) without triggering the usage recording? If yes, move recording server-side.
Recovery: Record usage in the API route that processes the actual action: await usage.record(workspaceId, { quotaSlug: 'api_calls', quantity: 1, idempotencyKey: requestId }).
---
Mistake 24: No idempotencyKey on Critical Usage Recordings
What the developer did: Calls usage.record(...) without an idempotencyKey for an action that might be retried (network error, user double-click, job retry).
Symptom: Usage is double-counted. A workspace's quota is consumed twice for a single action.
Why it happens: idempotencyKey is optional. Developers skip optional parameters.
Detection: Trigger the action twice in rapid succession (double-click). Check the quota usage counter — if it increments by 2, there's no idempotency protection.
Recovery: Add idempotencyKey: generateUniqueId() to every usage.record call for user-initiated or retryable actions. Use a UUID tied to the specific request (not the user ID or workspace ID).
---
Security Vulnerabilities (Mistakes 25–28)
Mistake 25: clientSecret Exposed to Browser
What the developer did: Used NEXT_PUBLIC_BUILDBASE_CLIENT_SECRET or imported process.env.BUILDBASE_CLIENT_SECRET in a client component.
Symptom: No runtime error — the app works. But the secret is embedded in the JavaScript bundle. Anyone can open DevTools → Sources → search for the secret.
Why it happens: Fast development pace. Env var copy-paste mistakes. No lint rule catches this.
Detection: Build the app (npm run build) and search the .next/static folder for the secret value. Or check grep -r "NEXT_PUBLIC" .env.local.
Recovery: Move to BUILDBASE_CLIENT_SECRET (no prefix). Use only in server-side code. Rotate the secret in Buildbase dashboard immediately if it was already deployed.
---
Mistake 26: sessionId in localStorage
What the developer did: Stored the sessionId in localStorage after receiving it from the /api/auth/token response.
Symptom: No immediate issue — auth works. But any XSS attack can read localStorage.getItem('sessionId') and steal the session.
Why it happens: JWT-in-localStorage is common in tutorials. Looks equivalent to cookie storage.
Detection: Check localStorage in DevTools. If sessionId or bb-session-id appears there, it's wrong.
Recovery: Never store the sessionId in localStorage. The /api/auth/token route sets an httpOnly cookie. The getSession callback calls /api/auth/session to read it server-side. JavaScript never touches the token directly.
---
Mistake 27: No Auth Check in API Routes
What the developer did: Built API routes that perform user-specific actions without checking if the user is authenticated first.
Symptom: Unauthenticated requests can access protected data or perform privileged operations.
Why it happens: The SDK handles client-side auth state. Developers assume the auth guard at the React layer is sufficient.
Detection: Call a protected API route without a session cookie. If you get data back, there's no server-side auth check.
Recovery: At the top of every protected API route: const session = await auth(); if (!session) return Response.json({ error: 'Unauthorized' }, { status: 401 });. The auth() function from the factory reads the session cookie and validates it.
---
Mistake 28: Webhook Not Signature-Verified
What the developer did: Built a webhook handler that processes events without verifying the x-buildbase-signature header.
Symptom: No immediate issue — the app works. But any malicious party can send fake webhook events (fake subscription.created, fake subscription.canceled) to your endpoint.
Why it happens: Signature verification feels like an optional hardening step.
Detection: Send a POST request to your webhook endpoint with a fake payload and no signature header. If it processes the event, verification is missing.
Recovery: Verify before processing any event. Both webhook helpers take a single options object (not positional args) and require the x-buildbase-timestamp header for replay protection. Prefer parseWebhookEvent — it verifies and parses in one step, returning null on failure:
const event = parseWebhookEvent({
body: rawBody,
signature: request.headers.get('x-buildbase-signature'),
timestamp: request.headers.get('x-buildbase-timestamp'),
secret: process.env.BUILDBASE_WEBHOOK_SECRET!,
});
if (!event) return Response.json({ error: 'Invalid webhook' }, { status: 401 });
// event.event is the type string; event.data is the payload---
Performance & Architecture (Mistakes 29–30)
Mistake 29: BuildBase() Called Per Request Instead of as a Singleton
What the developer did: Called BuildBase({ ... }) inside a function, API route handler, or React component, creating a new factory instance on every call.
Symptom: Performance degrades under load. Each request creates new connection pools and configuration objects. In development, this may not be noticeable.
Why it happens: Developers follow the pattern of other libraries that are initialized per-request. Or they create the factory inside a component to access env vars.
Detection: Search for BuildBase( in your codebase. Any occurrence outside a module-level const assignment is a problem.
Recovery: Call BuildBase(...) exactly once at module level in lib/buildbase.ts. Export the destructured action modules. Import them in API routes and server components. The factory is a singleton — one instance for the lifetime of the process.
---
Mistake 30: Excessive Re-renders from Subscription Context
What the developer did: Read useSubscriptionContext() in many deeply nested components, or structured component trees to cause re-renders whenever subscription data updates.
Symptom: The app re-renders frequently when subscription data loads or refetches. Noticeable performance issues on slow networks where subscription data refetches often.
Why it happens: React context causes all consumers to re-render when context changes. Subscription data refetches on workspace change and on a regular poll interval.
Detection: Use React DevTools Profiler. If many components highlight on subscription data updates, context consumption is too broad.
Recovery: Memoize components that use subscription data with React.memo. Extract subscription checks into a single gate component (WhenSubscription) rather than reading the raw context in many places. Use gate components for rendering decisions — they're optimized for this. Access the raw context only when you genuinely need the subscription data object, not just a boolean.
Frequently Asked Questions
Contents
- Installation & Setup — Next.js, React, Express, Stripe
- Authentication — OAuth, sessions, staying logged in
- Workspaces — multi-workspace and creation rules
- Billing — pricing pages, upgrades, free plans
- Feature Flags — creating and plan-linking flags
- Quota & Credits — resets, overage, expiration
- i18n — adding languages and RTL support
- Notifications — email providers, unsubscribe, push
Installation & Setup
Q: Can I use this with Next.js? Yes. The SDK is designed for Next.js. Use 'use client' in components using SDK hooks. The server-side BuildBase() factory works in API routes and Server Components. See nextjs-integration.md for the complete pattern.
Q: Can I use this with React 18 instead of 19? Yes. The peer dependency is react@^18.0.0 || ^19.0.0.
Q: Can I use the server-side SDK with Express? Yes. Don't set getSessionId in the config. Use withSession(req.headers['x-session-id']) per request to get scoped action modules.
Q: Do I need to install Stripe separately? No. Stripe is handled by Buildbase. You connect your Stripe account in the Buildbase dashboard. No Stripe SDK in your app.
---
Authentication
Q: How do I handle OAuth with multiple providers (Google + GitHub)? Auth method configuration is done in the Buildbase dashboard. Your code doesn't change — the SDK redirects to the Buildbase login page which handles multiple providers.
Q: Can I use the Buildbase SDK alongside next-auth? Yes. They use different cookie names and manage different sessions. Buildbase handles the SaaS platform layer; next-auth can handle your own user auth if needed.
Q: How do I stay logged in after page refresh? This is handled by getSession callback. The SDK calls it on mount and restores the session from your httpOnly cookie. The session is valid for as long as your cookie's maxAge (recommend 7 days).
Q: The user gets logged out after they close the browser. Why? Check your cookie's maxAge. If you set it to a session cookie (no maxAge), it expires when the browser closes. Set maxAge: 60 * 60 * 24 * 7 for 7 days.
---
Workspaces
Q: Can a user be in multiple workspaces simultaneously? No. The SDK manages one currentWorkspace at a time. Use switchToWorkspace(workspace) — pass the workspace object (not an id) — to change.
Q: How do I auto-select the first workspace on login? The SDK does this automatically when autoCreateFirstWorkspace is enabled in dashboard settings. You can also implement it using handleEvent:
handleEvent: async (type, data) => {
if (type === 'user:created' || type === 'workspace:created') {
// First workspace was created — no action needed, SDK auto-selects
}
}Q: Can I restrict workspace creation to admins only? Yes — configure "Can Create Workspace" to "Owner Only" or "Disabled" in the Buildbase dashboard. No code change needed.
---
Billing
Q: How do I show a pricing page to unauthenticated users? Use the PricingPage component with a redirectBaseUrl:
<PricingPage slug="main-pricing" redirectBaseUrl="https://app.com/dashboard">
{({ plans, selectPlan, loading }) => { ... }}
</PricingPage>selectPlan() handles the "redirect to sign in and come back" flow automatically.
Q: How do I know when a user upgrades their plan? Listen for subscription webhook events from Buildbase, or poll subscription.get(workspaceId) after the user returns from checkout.
Q: How do I implement a free plan? Create a free plan in the dashboard with price $0. Workspaces can subscribe to it. Or use WhenNoSubscription to show free-tier content.
---
Feature Flags
Q: How do I create a feature flag? In the Buildbase dashboard → Features → New Feature. Define a name and slug. Then use the slug in your code.
Q: Can feature flags be set per-workspace automatically based on plan? Yes. In the dashboard, associate features with plans. When a workspace upgrades, features are automatically enabled.
---
Quota & Credits
Q: Can quotas reset on a custom date instead of billing cycle? No — quotas reset at the start of each billing period (as defined by Stripe).
Q: What happens when a workspace runs out of quota?
- If overage is configured on the plan: usage continues, billed per-unit
- If no overage:
availablereturns 0,hasOverageis false. Your code should gate actions usingWhenQuotaAvailableor checkavailable > 0before proceeding.
Q: Can credits expire? Yes — credit buckets can have expiration dates. Use credits.getExpiring(workspaceId, 7) to check credits expiring in the next 7 days.
---
i18n
Q: How do I add the SDK language support? Set the locale prop on SaaSOSProvider:
<SaaSOSProvider locale="fr">{/* SDK UI renders in French */}</SaaSOSProvider>Supported: en, es, fr, de, ja, zh, hi, ar
Q: Does RTL (Arabic) work automatically? Yes. The dir attribute is set correctly on all SDK dialogs and components when locale="ar".
---
Notifications
Q: Do I need to set up an email provider? Yes — configure your email provider (Resend, SendGrid, etc.) in the Buildbase dashboard. The SDK notification.send() calls Buildbase which routes through your configured provider.
Q: Can users unsubscribe from email notifications? Yes — automatically. The workspace settings dialog includes unsubscribe options for events with userManaged: true. Buildbase handles the unsubscribe tracking.
Q: Is push notification support required? No — it's optional. If you don't create public/push-sw.js, push simply won't work but won't break anything.
Glossary
Org (Organization) — A company or product that uses Buildbase. Identified by orgId. Contains all workspaces, users, plans, and settings.
Workspace — The tenant/team unit. Subscriptions, quotas, credits, and feature flags belong to workspaces. Similar to a "team" in Slack or "organization" in GitHub.
User — A person who authenticates. Can belong to multiple workspaces with different roles in each.
Session — Authentication state for a user. Represented as a sessionId — an opaque token string issued by the Buildbase server after OAuth login.
sessionId — The Buildbase authentication token. Stored in an httpOnly cookie. Used in all SDK API calls.
SaaSOSProvider — The root React provider for the Buildbase SDK. Must wrap your entire application. Configures auth, server URL, org ID, and locale.
BuildBase() — The server-side factory function. Called once to create action modules (workspace, subscription, usage, etc.) for API routes and background jobs.
Plan — A subscription tier (Free, Pro, Enterprise). Defined in Buildbase dashboard, connected to Stripe. Plans have prices, quotas, features, and trial settings.
Plan Group — A collection of plans shown together (e.g., "main-pricing" might have Free, Pro, Enterprise). Referenced by slug in usePublicPlans and PricingPage.
Quota — A usage limit included in a plan (API calls, storage, emails). Resets each billing period. Can have overage pricing.
Quota Slug — The identifier for a quota (e.g., api_calls, emails, storage). Must match dashboard configuration.
Credit — A prepaid unit purchased separately from subscriptions. Does not reset automatically. Used for AI tokens, compute, one-off actions.
Feature Flag — A boolean toggle per workspace or user. Defined in dashboard, toggled via SDK or dashboard.
Gate Component — A conditional render component that shows/hides content based on state. Examples: WhenSubscription, WhenQuotaAvailable, WhenCreditsAvailable.
ClientId — OAuth app identifier. Public-safe (used on client side). Created in Buildbase dashboard → Auth.
ClientSecret — OAuth app secret. Server-side only. NEVER expose to client. Used in /api/auth/token endpoint.
redirectUrl — The URL Buildbase sends users back to after OAuth login. Must match what's configured in the OAuth app.
httpOnly Cookie — A browser cookie not readable by JavaScript. Used to store sessionId securely (prevents XSS attacks on session tokens).
Pricing Variant — A currency-specific version of a plan's pricing. Enables multi-currency billing.
Overage — Usage beyond the included quota amount. Billed per-unit automatically via Stripe if configured.
Workspace Mode — Either "Personal" (one user, one workspace) or "Platform" (multi-user, multi-workspace). Set in dashboard.
Service Session — A sessionId used for background jobs/service accounts, not tied to a specific logged-in user. Obtained via the token exchange endpoint.
Token Exchange — The process of swapping code (from OAuth redirect) for a sessionId. Happens server-side in /api/auth/token.
RBAC — Role-Based Access Control. Users have a role within each workspace. Roles are developer-defined strings (commonly owner, admin, member) configured in the dashboard — not a fixed built-in set. Gates: WhenWorkspaceRoles.
Event Emitter — The SDK's internal system for dispatching events (workspace:changed, user:created, etc.) that can be listened to via handleEvent callback.
Workspace Settings Provider — Internal SDK provider that powers the built-in workspace settings dialog (users, billing, features, permissions).
Push Service Worker — A JavaScript file at public/push-sw.js required for browser push notifications. Provided verbatim in the SDK documentation.
Ad-hoc Notification — A push notification sent with any event slug without pre-registering the event in the dashboard.
HTTP API — Endpoint Catalog
Every endpoint the @buildbase/sdk calls, extracted from source (sdk/src/api/services/*.ts, sdk/src/lib/server-client.ts). Paths below are the part after the base. Unless noted, the full URL is:
{serverUrl}/api/{version}/public/{path} e.g. https://api.console.buildbase.app/api/v1/public/workspacesAll authenticated calls send x-session-id: <sessionId>. Bodies are JSON (Content-Type: application/json). See overview.md for envelope/error rules.
Contents
- Auth — login request and code→session exchange
- Profile & Users — profile, attributes, user features
- Settings (org-scoped, public path) — org/OS settings
- Workspaces & Members — CRUD and membership
- Features — workspace feature definitions and toggles
- Subscription — checkout, upgrade, cancel, billing portal
- Plans — plan groups and public plan lookups
- Invoices — list and fetch invoices
- Usage / Quota — record usage and quota status
- Credits — balance, consume, purchase, transactions
- Notifications — send events, events, preferences
- Push — VAPID key, subscribe, unsubscribe
- Permissions — local computation from three GETs
---
Auth
/auth/request is the one endpoint that sits outside /public.
| Purpose | Method | Path | Auth | Body / Query | Response |
|---|---|---|---|---|---|
| Start login — get the provider redirect URL | POST | /api/{version}/auth/request | none | { orgId, clientId, redirect: { success, error } } | { success, data: { redirectUrl }, message } |
| Get current user's profile (also validates the session) | GET | public/profile | x-session-id | — | IUser |
The secure code→session exchange (POST /api/v1/auth/tokenwithclientId+clientSecret+orgId+code→{ data: { sessionId, user } }) is performed server-side and is how the official Next.js starter obtains thesessionId. It is not called by the SDK client package itself but is the correct server flow for any backend — see using-from-any-language.md.
---
Profile & Users
| Purpose | Method | Path | Body / Query | Response |
|---|---|---|---|---|
| Get profile | GET | public/profile | — | IUser |
| Update profile | PATCH | public/profile | Partial<IUser> | IUser |
| Get user attributes | GET | public/users/attributes | — | `Record<string, string\ |
| Bulk-update attributes | PATCH | public/users/attributes | { attributes: Record<string, …> } | IUser |
| Update one attribute | PATCH | public/users/attributes/{attributeKey} | { value } | IUser |
| Get resolved user feature flags | GET | public/users/features | — | Record<string, boolean> |
IUser: { _id, name, email, image?, role, country?, timezone?, language?, currency?, attributes?, createdAt, updatedAt }.
---
Settings (org-scoped, public path)
| Purpose | Method | Path | Response |
|---|---|---|---|
| Get org/OS settings | GET | public/{orgId}/settings | ISettings (includes the workspace permission template) |
---
Workspaces & Members
| Purpose | Method | Path | Body | Response |
|---|---|---|---|---|
| List workspaces | GET | public/workspaces | — | IWorkspace[] |
| Create workspace | POST | public/workspaces | { name, image? } | IWorkspace |
| Get one workspace | GET | public/workspaces/{workspaceId} | — | IWorkspace |
| Update workspace | PUT | public/workspaces/{id} | Partial<IWorkspace> | IWorkspace |
| Delete workspace | DELETE | public/workspaces/{id} | — | { success } |
| List members | GET | public/workspaces/{workspaceId}/users | — | IWorkspaceUser[] |
| Invite / add member | POST | public/workspaces/{workspaceId}/users/add | { email, role } | { userId, workspace, message } |
| Remove member | DELETE | public/workspaces/{workspaceId}/users/{userId} | — | { userId, workspace, message } |
| Update member (role) | PATCH | public/workspaces/{workspaceId}/users/{userId} | Partial<IWorkspaceUser> | { userId, workspace, message } |
| Update workspace settings (permissions) | PATCH | public/workspaces/settings | { permissions: Record<role, string[]> } | — |
| Update workspace permission matrix | PATCH | public/workspaces/{workspaceId}/permissions | { permissions: Record<role, string[]> } | — |
---
Features
| Purpose | Method | Path | Body | Response |
|---|---|---|---|---|
| List workspace feature definitions | GET | public/workspaces/features | — | IWorkspaceFeature[] |
| Toggle a workspace feature | PATCH | public/workspaces/{workspaceId}/features | { features: { [slug]: boolean } } | IWorkspace |
| Get resolved user features | GET | public/users/features | — | Record<string, boolean> |
There is no "check" endpoint — fetch the map and look up the slug. A feature is on if present and true.
---
Subscription
| Purpose | Method | Path | Body | Response |
|---|---|---|---|---|
| Get current subscription | GET | public/workspaces/{workspaceId}/subscription | — | ISubscriptionResponse |
| Create checkout session | POST | public/workspaces/{workspaceId}/subscription/checkout | { planVersionId, billingInterval?, currency?, successUrl?, cancelUrl?, stripeOptions? } | CheckoutResult (checkout / trial_started / existing) |
| Select a free plan | POST | public/workspaces/{workspaceId}/subscription/select-free-plan | { planVersionId } | { success, message } |
| Update (up/downgrade) | PATCH | public/workspaces/{workspaceId}/subscription | { planVersionId, billingInterval?, successUrl?, cancelUrl? } | update result or checkout-session response if payment needed |
| Cancel at period end | POST | public/workspaces/{workspaceId}/subscription/cancel-at-period-end | — | ISubscriptionResponse |
| Resume | POST | public/workspaces/{workspaceId}/subscription/resume | — | ISubscriptionResponse |
| Stripe billing-portal URL | POST | public/workspaces/{workspaceId}/subscription/billing-portal | { returnUrl? } | { url } |
---
Plans
| Purpose | Method | Path | Auth | Response |
|---|---|---|---|---|
| Get plan group (current/latest) | GET | public/workspaces/{workspaceId}/subscription/plan-group | session | IPlanGroupResponse |
| Plan group at a version | GET | public/workspaces/{workspaceId}/subscription/plan-group?groupVersionId={id} | session | IPlanGroupResponse |
| List group versions | GET | public/workspaces/{workspaceId}/subscription/plan-group/versions | session | IPlanGroupVersionsResponse |
| Public plans by slug | GET | public/{orgId}/plans/{slug} | none | IPublicPlansResponse (prices in cents) |
| Public plan-group-version by id | GET | public/plan-group-versions/{groupVersionId} | none | IPlanGroupVersion |
---
Invoices
| Purpose | Method | Path | Query | Response |
|---|---|---|---|---|
| List invoices | GET | public/workspaces/{workspaceId}/subscription/invoices | limit (default 10), starting_after? | IInvoiceListResponse { invoices[], has_more } |
| Get invoice | GET | public/workspaces/{workspaceId}/subscription/invoices/{invoiceId} | — | IInvoiceResponse |
IInvoice: { id, number, amount_due, amount_paid (cents), currency, status, created, due_date, hosted_invoice_url, invoice_pdf, description, subscription }.
---
Usage / Quota
| Purpose | Method | Path | Body / Query | Response |
|---|---|---|---|---|
| Record usage | POST | public/workspaces/{workspaceId}/subscription/usage | { quotaSlug, quantity, metadata?, source?, idempotencyKey? } | { used, consumed, included, available, overage, billedAsync } |
| Record usage batch (≤100) | POST | public/workspaces/{workspaceId}/subscription/usage/batch | { items: [{ quotaSlug, quantity, metadata?, source?, idempotencyKey? }] } | { success, total, succeeded, failed, results[] } |
| One quota status | GET | public/workspaces/{workspaceId}/subscription/usage/status?quotaSlug={slug} | — | { quotaSlug, consumed, included, available, overage, hasOverage, allowOverage? } |
| All quota status | GET | public/workspaces/{workspaceId}/subscription/usage/all | — | { quotas: Record<slug, status> } |
| Usage logs | GET | public/workspaces/{workspaceId}/subscription/usage/logs | quotaSlug?, from?, to?, source?, page?, limit? | paginated { docs[], totalDocs, page, totalPages, … } |
---
Credits
| Purpose | Method | Path | Body / Query | Response |
|---|---|---|---|---|
| Get balance | GET | public/workspaces/{workspaceId}/credits | — | { available, totalGranted, totalConsumed, totalExpired, totalRefunded } |
| Consume credits | POST | public/workspaces/{workspaceId}/credits/consume | { amount, description?, idempotencyKey?, metadata? } | { success, consumed, balanceAfter } — 402 → insufficient ({ available, requested }) |
| Purchase package | POST | public/workspaces/{workspaceId}/credits/purchase | { creditPackageId, successUrl, cancelUrl, currency? } | { sessionId, url } |
| List packages | GET | public/workspaces/{workspaceId}/credits/packages | — | ICreditPackage[] — raw wire response may be paginated { docs: ICreditPackage[] } (the SDK flattens docs ?? data) |
| Transactions | GET | public/workspaces/{workspaceId}/credits/transactions | type?, page?, limit? | paginated |
| Buckets | GET | public/workspaces/{workspaceId}/credits/buckets | status?, source?, page?, limit? | paginated |
| Expiring credits | GET | public/workspaces/{workspaceId}/credits/expiring?days={n} | days? (1–90, default 7) | { days, expiringCredits, buckets[] } |
| Public packages by org | GET | public/{orgId}/credit-packages | none | IPublicCreditPackagesResponse |
---
Notifications
| Purpose | Method | Path | Body | Response |
|---|---|---|---|---|
| Send/trigger an event | POST | public/workspaces/{workspaceId}/notifications/send | { event, userId?, data? } (omit userId → notify all members) | { sent, channels: { email, push }, notifiedCount?, reason? } |
| List manageable events | GET | public/workspaces/{workspaceId}/notification-events | — | NotificationEvent[] |
| Get preferences | GET | public/workspaces/{workspaceId}/notification-preferences | — | wire response is wrapped: { notificationPreferences: Record<slug, { email?, push? }> } (the SDK unwraps .notificationPreferences) |
| Update preferences | PATCH | public/workspaces/{workspaceId}/notification-preferences | { notificationPreferences: Record<slug, { email?, push? }> } | same wrapped shape as GET |
data (NotificationData) supports: title, message, icon, image, badge, url, tag, actions[≤2], silent, requireInteraction, renotify, timestamp, dir, ttl, urgency, scheduledAt, channels, plus arbitrary merge-tag keys.
---
Push
PushApi requires orgId to be configured.
| Purpose | Method | Path | Body | Response |
|---|---|---|---|---|
| Get VAPID public key | GET | public/push/vapid-public-key | — | { publicKey } |
| Subscribe a device | POST | public/push/subscribe | { endpoint, keys: { p256dh, auth }, userAgent } | empty (server may send a welcome push) |
| Unsubscribe a device | DELETE | public/push/unsubscribe | { endpoint } | empty |
---
Permissions
There is no permission-check endpoint. The SDK computes permissions locally from three GETs:
1. GET public/workspaces/{workspaceId} → the workspace (has permissions: Record<role, string[]>) 2. GET public/{orgId}/settings → org settings (workspace permission template) 3. GET public/workspaces/{workspaceId}/users → to find the caller's role
Then: find the user's role in (3), and check whether the requested permission is in the role's set from (1)/(2). Platform permissions look like workspace:*; app permissions are your own strings (e.g. reports:export). The write side is PATCH public/workspaces/{workspaceId}/permissions. To do this from another language, fetch the three and replicate the lookup — there is nothing to call for a single boolean answer.
HTTP API — Overview (use Buildbase from any language)
The @buildbase/sdk package is a convenience wrapper around a plain HTTP+JSON API. Nothing about that API is JavaScript-specific — there's no request signing, no cookies required, no client-side crypto. Any backend language (Python, Go, Ruby, PHP, Java, C#, Rust…) can call it with an HTTP client and a session token in a header.
This section was reverse-engineered directly from the SDK source (sdk/src/lib/api-base.ts, sdk/src/api/services/*.ts, sdk/src/lib/server-client.ts), so it reflects exactly what the SDK actually sends — not docs that may drift.
Read next: endpoints.md (the full endpoint catalog), webhooks.md (verify inbound webhooks in any language), using-from-any-language.md (auth flow + Python/Go examples).
---
1. Base URL
{serverUrl}/api/{version}/{basePath}/{path}serverUrl— the hosted value is `https://api.console.buildbase.app` (or your own origin if self-hosting).version—v1.basePath— almost always `public`. (betaexists for a few beta endpoints; auth's/auth/requestis the one exception that sits outside basePath.)
So a typical call is: https://api.console.buildbase.app/api/v1/public/workspaces.
2. Authentication — one header
x-session-id: <sessionId>That's the whole auth scheme for normal calls. The sessionId is obtained once via the login/code-exchange flow (see using-from-any-language.md) and then sent on every request. There is no `Authorization` header, no bearer prefix, no request signing.
`orgId` is never a header. Depending on the endpoint it appears as:
- a path segment for public/unauthenticated endpoints:
/api/v1/public/{orgId}/plans/{slug},/api/v1/public/{orgId}/credit-packages,/api/v1/public/{orgId}/settings - a query param for beta config (
?orgId=...) - a body field for the OAuth
auth/requestcall
For all normal authenticated calls, orgId is not sent at all — the server infers the org from the session.
3. Standard headers
| Header | When |
|---|---|
x-session-id: <token> | whenever you have a session (all authenticated calls) |
Content-Type: application/json | only when sending a body |
No Accept, no User-Agent required. You may add your own custom headers freely.
4. Request & response format
- Request bodies are JSON. GET parameters are query-string (
?quotaSlug=...&page=1&limit=20). - Path IDs are interpolated into the URL. Note: in the SDK,
slug,groupVersionId, andquotaSlugare URL-encoded; other IDs (workspaceId,userId,invoiceId) are not. When replicating, URL-encode any value that could contain special characters to be safe. - Responses come in two shapes, depending on endpoint:
1. Bare JSON — the body is the object (most endpoints). 2. Enveloped — { "success": true, "data": { ... }, "message": "..." }. When success is present and false, treat it as an error using message.
A robust client handles both: if the JSON has a success field, unwrap data; otherwise use the body directly.
5. Errors
- Non-2xx responses carry a JSON body shaped like
{ "message": "..." }or{ "error": "..." }. Readmessagefirst, thenerror. - 401 means the session is invalid/expired — re-authenticate. (The SDK fires an
onUnauthorizedcallback; there is no automatic token refresh — sessions don't refresh, you re-login.) - 402 on
credits/consumespecifically means insufficient credits; the body includesavailableandrequested. (The SDK surfaces this as error codeINSUFFICIENT_CREDITS.) - The SDK retries only on 5xx and network errors (never 4xx), with exponential backoff, and only if you opt in (
maxRetries). Default timeout 30s.
6. Idempotency
There is no idempotency header. Idempotency is an optional body field idempotencyKey on the write endpoints that support it: usage (single + per-item in batch) and credits/consume. Send a stable unique key to make a write safe to retry without double-applying.
7. What is NOT pure HTTP (replication caveats)
Almost everything ports cleanly. The few things to know:
- Permissions are computed client-side, not via an endpoint.
permissions.check/permissions.resolvein the SDK fetch three endpoints —GET workspaces/{id},GET settings,GET workspaces/{id}/users— then combine the role→permissions matrix locally. To replicate in another language you must fetch those three and apply the same logic (find the user's role in the workspace, union the platformworkspace:*permissions defined on the workspace/settings with the role's app permissions). See endpoints.md for the inputs. - The browser login UX (redirect handling, reading
?code=off the URL, localStorage) is browser glue. On a backend you do the equivalent server-side: receive the?code=at your redirect route and exchange it (see using-from-any-language.md). - Webhook verification is HMAC-SHA256 — every language has this in its standard library. Recipe in webhooks.md.
That's it. There is no proprietary protocol — if you can make HTTPS requests and compute an HMAC, you can use all of Buildbase from any language.
Using Buildbase from Any Backend Language
There is no official Buildbase SDK for Python, Go, Ruby, PHP, Java, etc. — but you don't need one. The SDK is a thin wrapper over HTTP+JSON, so you can do everything it does with your language's HTTP client plus one header. This guide shows the end-to-end flow and minimal examples.
Read overview.md and endpoints.md for the full contract; webhooks.md for inbound verification.
Contents
- The whole model in four facts — core concepts in brief
- Step 1 — Log the user in and get a `sessionId` — OAuth-style login flow
- Step 2 — Call any endpoint — Python and Go examples
- Step 3 — Server-to-server / background jobs (no user) — service-account sessions
- Step 4 — Webhooks — verifying inbound webhooks
- What you must implement yourself (no single endpoint) — permission and feature checks
- Honest limits — caveats about this reference
---
The whole model in four facts
1. Base URL: https://api.console.buildbase.app/api/v1/public/<path> (the hosted value; your own origin if self-hosting). 2. Auth: one header — x-session-id: <sessionId> — on every authenticated call. 3. Data: JSON bodies; query strings for GET params. Responses are JSON, sometimes wrapped in { success, data, message }. 4. You need two secrets from the dashboard (console.buildbase.app): clientId + clientSecret (for the login exchange) and your orgId.
The only real "flow" is getting a sessionId for a user. After that, it's just authenticated HTTP.
---
Step 1 — Log the user in and get a sessionId
This is the OAuth-style flow. Your backend acts as the confidential client (it holds the clientSecret).
1. Send the user to Buildbase's login. Either redirect them to your hosted login URL with your clientId and a redirect target, or call POST /api/v1/auth/request with { orgId, clientId, redirect: { success, error } } and redirect the browser to the returned data.redirectUrl. 2. User authenticates on Buildbase, then is redirected back to your redirect.success URL with a one-time ?code=.... 3. Exchange the code for a session, server-side. POST to the Buildbase token endpoint with your secret:
POST https://api.console.buildbase.app/api/v1/auth/token
Content-Type: application/json
{ "code": "<from ?code>", "clientId": "...", "clientSecret": "...", "orgId": "..." }Response: { "data": { "sessionId": "...", "user": { ... } } }.
Source note: this token-exchange endpoint + response shape is taken from the official Next.js starter's server route, not from the SDK client package (the browser SDK uses a simpler client-only path). For a backend in any language, this server-side exchange is the correct, secure flow because your backend can safely hold the clientSecret.4. Store the `sessionId` in your own session store / signed cookie (treat it like a session token — keep it server-side, e.g. an httpOnly cookie). 5. Use it on every subsequent call as x-session-id.
To validate a session later, call GET /api/v1/public/profile with the header — a 401 means it's no longer valid (re-login; sessions don't auto-refresh).
---
Step 2 — Call any endpoint
Pick from endpoints.md. Examples:
Python (requests)
import requests
BASE = "https://api.console.buildbase.app/api/v1/public"
def bb_get(path, session_id, **params):
r = requests.get(f"{BASE}/{path}", headers={"x-session-id": session_id}, params=params)
r.raise_for_status()
body = r.json()
return body.get("data", body) if isinstance(body, dict) and "success" in body else body
def bb_post(path, session_id, payload):
r = requests.post(f"{BASE}/{path}",
headers={"x-session-id": session_id, "Content-Type": "application/json"},
json=payload)
r.raise_for_status()
body = r.json()
return body.get("data", body) if isinstance(body, dict) and "success" in body else body
# List the user's workspaces
workspaces = bb_get("workspaces", session_id)
# Record metered usage for a workspace (idempotent)
bb_post(f"workspaces/{ws_id}/subscription/usage", session_id, {
"quotaSlug": "api_calls", "quantity": 1, "idempotencyKey": request_id,
})
# Consume 5 prepaid credits (handle 402 = insufficient)
import requests as _rq
resp = _rq.post(f"{BASE}/workspaces/{ws_id}/credits/consume",
headers={"x-session-id": session_id, "Content-Type": "application/json"},
json={"amount": 5, "idempotencyKey": gen_id})
if resp.status_code == 402:
info = resp.json() # { available, requested } → show "buy more credits"Go (net/http)
func bbGet(path, sessionID string) (*http.Response, error) {
req, _ := http.NewRequest("GET", "https://api.console.buildbase.app/api/v1/public/"+path, nil)
req.Header.Set("x-session-id", sessionID)
return http.DefaultClient.Do(req)
}
func bbPost(path, sessionID string, body []byte) (*http.Response, error) {
req, _ := http.NewRequest("POST", "https://api.console.buildbase.app/api/v1/public/"+path,
bytes.NewReader(body))
req.Header.Set("x-session-id", sessionID)
req.Header.Set("Content-Type", "application/json")
return http.DefaultClient.Do(req)
}Any language with an HTTP client works the same way — set x-session-id, send/parse JSON, unwrap { success, data } if present.
---
Step 3 — Server-to-server / background jobs (no user)
For cron jobs or service-to-service calls, use a service-account session ID (obtained the same way, for a service user) and send it as x-session-id. This mirrors what the Node SDK's withSession(serviceSessionId) does — there's nothing Node-specific about it.
---
Step 4 — Webhooks
Verify inbound webhooks with HMAC-SHA256 — full recipe + Python/Go code in webhooks.md.
---
What you must implement yourself (no single endpoint)
- Permission checks. There's no "can this user do X?" endpoint. Fetch
GET workspaces/{id},GET {orgId}/settings, andGET workspaces/{id}/users, find the caller's role, and check the requested permission against that role's set (platformworkspace:*perms + your app perms). See endpoints.md. - Feature/quota "checks." Fetch the feature map (
users/featuresorworkspaces/features) or quota status, then decide in your code. There's no boolean-check endpoint.
---
Honest limits
- The endpoint catalog here is reverse-engineered from the SDK source (accurate for what the SDK sends), but Buildbase's server may expose more endpoints or fields than the SDK uses. The fully authoritative HTTP reference is the npm package README, which was not publicly fetchable at the time of writing — if something here is incomplete, that's the place to confirm.
- Request/response field lists reflect what the SDK's TypeScript types declare; servers can return extra fields. Treat responses leniently (ignore unknown fields).
- The token-exchange endpoint shape (
/api/v1/auth/token) comes from the official starter app, not the SDK package — verify against your dashboard's auth settings if it differs.
Webhooks — Verify in Any Language
Buildbase POSTs webhook events to an endpoint you host. Before trusting one, verify its signature. The SDK's verifyWebhookSignature / parseWebhookEvent do this in Node, but the algorithm is plain HMAC-SHA256 — reproducible in any language. This recipe is extracted verbatim from sdk/src/lib/webhook-verification.ts.
The algorithm
- Headers on the incoming request:
x-buildbase-signature— formatsha256=<hex>x-buildbase-timestamp— Unix epoch in seconds (integer)- Signed message: the literal string `
${timestamp}.${rawBody}— the timestamp header value, a.`, then the raw request body exactly as received (do not re-serialize parsed JSON; byte-for-byte matters). - MAC:
HMAC_SHA256(key = your_webhook_secret, message = "{timestamp}.{rawBody}"), hex-encoded (lowercase). - Compare: constant-time equality between the hex from the header (after stripping
sha256=) and your computed hex. - Replay window: reject if
abs(now_seconds - timestamp) > 300(5 minutes). Configurable;0disables the check. - Fail closed: reject if any of signature / timestamp / secret / body is missing, the prefix is wrong, the timestamp isn't a number, it's expired, or any error occurs.
Steps
1. Read the raw body (before JSON parsing) and the two headers. 2. Require x-buildbase-signature to start with sha256=; take the rest as sig_hex. 3. Parse the timestamp as an integer; reject if |now - ts| > 300. 4. Compute expected = hmac_sha256_hex(secret, f"{ts}.{rawBody}"). 5. Constant-time compare sig_hex vs expected. Accept only if equal. 6. Only then JSON.parse the body. The event shape is { event: string, timestamp: number, data: {...} } — switch on event (e.g. subscription.created, workspace.member_added). Dedupe on the event's id if you process at-least-once.
Python
import hmac, hashlib, time
def verify_buildbase_webhook(raw_body: str, sig_header: str, ts_header: str,
secret: str, max_age_seconds: int = 300) -> bool:
if not (raw_body and sig_header and ts_header and secret):
return False
if not sig_header.startswith("sha256="):
return False
sig_hex = sig_header[len("sha256="):]
try:
ts = int(ts_header)
except ValueError:
return False
if max_age_seconds > 0 and abs(int(time.time()) - ts) > max_age_seconds:
return False
expected = hmac.new(secret.encode(), f"{ts_header}.{raw_body}".encode(),
hashlib.sha256).hexdigest()
return hmac.compare_digest(sig_hex, expected)Use the raw ts_header string in the signed message (not a reformatted int).
Go
import ("crypto/hmac"; "crypto/sha256"; "encoding/hex"; "strconv"; "strings"; "time")
func VerifyBuildbaseWebhook(rawBody, sigHeader, tsHeader, secret string, maxAge int64) bool {
if rawBody == "" || sigHeader == "" || tsHeader == "" || secret == "" {
return false
}
if !strings.HasPrefix(sigHeader, "sha256=") {
return false
}
sigHex := strings.TrimPrefix(sigHeader, "sha256=")
ts, err := strconv.ParseInt(tsHeader, 10, 64)
if err != nil {
return false
}
if maxAge > 0 {
if d := time.Now().Unix() - ts; d > maxAge || -d > maxAge {
return false
}
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(tsHeader + "." + rawBody))
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sigHex), []byte(expected))
}Node (the SDK already does this)
In Node you can just use the SDK: parseWebhookEvent({ body, signature, timestamp, secret }) returns the parsed event or null. See knowledge/sdk/server-side.md.
Beginner Learning Path
A guided journey from zero to working Buildbase integration. Each milestone has a concrete deliverable, a "what success looks like" check, and checkpoint questions. Don't skip milestones — each one builds on the last.
This path is the journey and the checks. The full, commented code lives in one place: [quick-start.md](../sdk/quick-start.md). That is the canonical golden-path setup guide. When a milestone needs setup code, you'll paste it from there rather than from divergent copies here. This keeps you from accidentally mixing two slightly different versions.
Contents
- Milestone 0: Orient (Explorer) — what to read first, what to skip, the one mental model to internalize before code
- Milestone 1: First Working Auth (Beginner) — set up the project, then create the factory, three auth endpoints, and provider (all from quick-start.md); confirm you can sign in
- Milestone 2: First Gate (Beginner) — protect a page with
WhenAuthenticated, understand the three gate states - Milestone 3: First Workspace Feature (Builder) — create a feature flag in the dashboard, gate content, learn why dashboard-first matters
- Milestone 4: First Billing Gate (Builder) — connect Stripe, create a plan, gate by subscription and by plan, test the checkout flow
- What's Next — pointers to Advanced and Power User topics
---
Milestone 0: Orient (Explorer Stage)
Goal: Understand what you're building before you touch code.
What to read first
- The plain-language intro: explain-buildbase-simply.md — what Buildbase is, in jargon-free terms, with analogies
- The quick-start guide: quick-start.md — the canonical step-by-step setup you'll follow in Milestone 1
- This learning path
What NOT to read yet
- The full README (2900 lines — overwhelming)
- Individual hook API references (auth.md, billing.md, etc.)
- The troubleshooting guide
- Webhook documentation
These are reference documents. Read them when you need them, not before you start.
The one mental model to internalize before touching code
Dashboard + Code = Features.
Almost nothing in the SDK works without corresponding configuration in the Buildbase dashboard. Feature slugs, plan slugs, quota slugs — the code references them by name, but they must exist in the dashboard first.
A slug is a short lowercase id you create in the dashboard, like pro or analytics. Think of it like environment variables: you define the value in one place (the dashboard), reference it by name in another (your code). The dashboard is not optional for billing or feature flags.
What success looks like for Milestone 0
You haven't written code yet — success here is understanding. You're ready to move on when the "Dashboard + Code = Features" idea feels obvious, and you know where the quick-start guide is.
Checkpoint questions for Milestone 0
1. In one sentence, what does Buildbase do for your app? (Answer: it handles login, billing, and what users are allowed to do — see product-model.md.) 2. What must happen in the Buildbase dashboard before a feature flag works in code? (Answer: the feature must exist there first, with a slug your code references.)
Stretch questions (optional)
- Why can't you put
clientSecretin aNEXT_PUBLIC_env var? (Answer: anything prefixedNEXT_PUBLIC_is shipped to the browser where anyone can read it; the secret must stay server-side. Covered in quick-start.md Step 2.) - What is the
orgIdand where do you find it? (Answer: your organization's 24-character hex ID, from Dashboard → Settings → General. See the credentials table in quick-start.md.)
---
Milestone 1: First Working Auth (Beginner Stage)
Goal: A user can sign in, and their session persists on page refresh.
Step 0: Make sure you have the right kind of project
This path assumes a Next.js (App Router) + TypeScript app. If you don't already have one, create it:
npx create-next-app@latest my-app --typescript --app --src-dir --import-alias "@/*"
cd my-appThis sets up everything the code assumes: TypeScript, the App Router, a src/ folder, and the @/ import alias (which maps @/ to ./src/*, so import { x } from '@/lib/buildbase' finds src/lib/buildbase.ts). When prompted, the defaults are fine.
Step 1: Follow the quick-start to wire up auth
Rather than re-paste setup code here (and risk it drifting out of sync), follow [quick-start.md](../sdk/quick-start.md) Steps 1–7. Full, commented code for every file is there — paste from it directly. Here's the map of what you'll create and why, so you understand the journey:
| File | What it is | quick-start step |
|---|---|---|
.env.local | Your five credentials (server URL, orgId, clientId, redirectUrl, and the secret clientSecret) | Step 2 |
src/lib/buildbase.ts | The factory — a function you call once that returns your configured Buildbase tools for server code | Step 3 |
src/app/api/auth/token/route.ts | The most important file: trades the one-time login code for a sessionId and stores it in an httpOnly cookie (a cookie page scripts can't read, so it can't be stolen) | Step 4 |
src/app/api/auth/session/route.ts | Read back the session on every page load so login survives refresh | Step 4 |
src/app/api/auth/signout/route.ts | Clears the cookie to log out | Step 4 |
src/components/saas-provider.tsx | The React provider that wraps your app. Starts with 'use client' (tells Next.js this file runs in the browser; the SDK's hooks require it) | Step 5 |
src/app/layout.tsx | Wrap your app in the provider and import the SDK CSS (import '@buildbase/sdk/css') | Step 6 |
src/app/page.tsx | A test sign-in page | Step 7 |
Two things worth knowing as you go (both confirmed in quick-start.md):
- You don't write code to read the `?code=` from the URL — the SDK does it automatically when the provider loads. You just need a page at your
redirectUrl(the docs use/callback) that the provider wraps; quick-start Step 7 includes a tiny callback page. - The token endpoint returns the shape
{ data: { sessionId, user } }— that's why the token route readsdata.sessionId.
What success looks like for Milestone 1
Run npm run dev, open http://localhost:3000, and: 1. You see a Sign In button. 2. Clicking it sends you to the Buildbase login page. 3. After logging in, you're sent back to your app, which now greets you by name. 4. Refresh the page — you stay logged in. (That's the session cookie doing its job.)
If all four happen, auth works and you're done with Milestone 1.
Common blockers at this stage
"Nothing happens after clicking Sign In" → Check that /api/auth/token exists and returns a 200. Open the Network tab in DevTools. Also confirm your redirectUrl is in the OAuth App's allowed redirect URLs in the dashboard.
"Signed in but logged out on refresh" → Check that /api/auth/session is reading the correct cookie name, and that the cookie is actually set (DevTools → Application → Cookies).
"App crashes on load" → Most likely an invalid orgId (must be 24 hex chars) or a missing env var. Remember to restart npm run dev after editing .env.local.
"Auth modal doesn't look right / components are invisible" → Missing import '@buildbase/sdk/css' in the root layout.
(The quick-start has a fuller troubleshooting table — see its "If something didn't work" section.)
Checkpoint questions for Milestone 1
1. Why does the clientSecret live in the server route (/api/auth/token) and not in the browser provider? (Answer: the secret must never reach the browser; server routes never ship to the browser, so it's safe there.) 2. Where is the session stored so login survives a refresh? (Answer: in the httpOnly cookie set by the token route and read back by the session route.)
Stretch questions (optional)
- What does
handleAuthenticationreceive, and what must it return? (Answer: it receives the one-timecodefrom the login redirect and returns{ sessionId }. See the provider in quick-start.md Step 5.) - Why must the three auth routes and the factory all use the same cookie name? (Answer: they read and write the same cookie; a mismatch means one route can't find what another stored — hence the shared
SESSION_COOKIE_NAMEconstant.)
Heads up — you may see `JWT`, workspace tokens, or `onWorkspaceChange` mentioned elsewhere. Those are an advanced, optional topic for multi-workspace apps. You do not need a second auth token for this path. The core sessionId is all you need to sign in and stay signed in. Skip that material for now.---
Milestone 2: First Gate (Beginner Stage)
Goal: Protect a page so only authenticated users can see it.
A gate is a When… component that shows its children only when a condition is true (and shows nothing, or a fallback, otherwise).
Add WhenAuthenticated to protect a page
Remember the 'use client' line — it tells Next.js this file runs in the browser, which the SDK hooks require.
'use client';
import { WhenAuthenticated, WhenUnauthenticated, useSaaSAuth } from '@buildbase/sdk/react';
export default function DashboardPage() {
const { signIn } = useSaaSAuth();
return (
<>
<WhenUnauthenticated>
<div>
<h1>Please sign in</h1>
<button onClick={() => signIn()}>Sign In</button>
</div>
</WhenUnauthenticated>
<WhenAuthenticated>
<div>
<h1>Dashboard</h1>
<p>You are authenticated.</p>
</div>
</WhenAuthenticated>
</>
);
}Understanding the three states
Every gate component has three states:
1. Loading — data is being fetched from Buildbase. The gate renders null (invisible) unless you provide a loadingComponent. 2. Condition met — renders children. 3. Condition not met — renders null (invisible) unless you provide a fallbackComponent.
States 1 and 3 look identical without loadingComponent. This confuses beginners.
<WhenAuthenticated
loadingComponent={<p>Loading...</p>}
fallbackComponent={<button onClick={signIn}>Sign In</button>}
>
<Dashboard />
</WhenAuthenticated>Common confusion: "why does my gate show nothing?"
Check in this order: 1. Is isAuthenticated from useSaaSAuth() true? If no, the user isn't signed in. 2. Is there a loadingComponent? Without it, loading looks like "not authenticated." 3. Is the CSS imported? Without it, components may render invisible. 4. Is the component using 'use client'? Hooks don't work in server components.
What success looks like for Milestone 2
Open the protected page while signed out: you see the "Please sign in" block. Sign in, then open it again: you see the "Dashboard" block instead. The content swaps based purely on auth state — no manual checks in your own code.
Checkpoint questions for Milestone 2
1. What's the difference between loadingComponent and fallbackComponent? (Answer: loadingComponent shows while the SDK is still checking; fallbackComponent shows when the condition is not met.) 2. If WhenAuthenticated shows nothing even though you ARE signed in, what's the first thing to check? (Answer: whether it's stuck in the loading state — add a loadingComponent to tell loading apart from "not authenticated." Then check the CSS import.)
Stretch questions (optional)
- When would you read
useSaaSAuth().isAuthenticateddirectly instead of usingWhenAuthenticated? (Answer: when you need the boolean in your own logic — e.g. deciding what to fetch — rather than just showing/hiding JSX.) - Can you use
WhenAuthenticatedin a Next.js Server Component? (Answer: no — it relies on SDK hooks, which need'use client'.)
---
Milestone 3: First Workspace Feature (Builder Stage)
Goal: Gate content behind a feature flag that only specific workspaces have.
Step 1: Create the feature in the dashboard first
Open Buildbase dashboard → Features → New Feature.
- Name: "Analytics Dashboard"
- Slug:
analytics(a short lowercase id, no spaces — this is what your code will reference)
Save it.
Step 2: Add the feature to a plan
Go to Plans → [Your Plan] → Features → Add Feature → select "Analytics Dashboard".
Save.
Step 3: Gate content in code
'use client';
import { WhenWorkspaceFeatureEnabled } from '@buildbase/sdk/react';
export default function AnalyticsPage() {
return (
<WhenWorkspaceFeatureEnabled
slug="analytics"
fallbackComponent={<p>Upgrade your plan to access analytics.</p>}
>
<AnalyticsDashboard />
</WhenWorkspaceFeatureEnabled>
);
}Why dashboard-first matters
If you skip steps 1 and 2, the gate will silently show the fallback for all users — even if they're on the paid plan. No error. Just silence. This is the most common "feature flags don't work" issue.
The dashboard is the source of truth for what features exist. The code only references them by slug, and the slug in code must exactly match the slug in the dashboard.
What success looks like for Milestone 3
Sign in as a user whose workspace is on a plan that includes the analytics feature: the AnalyticsDashboard renders. Sign in as a user whose plan does not include it: they see the "Upgrade your plan" fallback (and if you removed the fallback, they'd see nothing at all). Same code, different result based on the workspace's plan.
Checkpoint questions for Milestone 3
1. What happens if the feature slug in code doesn't exactly match the slug in the dashboard? (Answer: the gate finds no matching feature and silently shows the fallback for everyone — no error.) 2. Why might a workspace on the "Pro" plan still not see the feature? (Answer: because the feature has to be added to the Pro plan in the dashboard — being on Pro isn't enough by itself.)
Stretch questions (optional)
- How is
WhenWorkspaceFeatureEnableddifferent fromWhenUserFeatureEnabled? (Answer: one checks a feature on the current workspace, the other on the individual user. See feature-flags.md for the distinction.) - Can you enable a feature for one specific workspace without it being on their plan? (Answer: this is an override-style case — check feature-flags.md before relying on it.)
---
Milestone 4: First Billing Gate (Builder Stage)
Goal: Show different content to paid vs free users. Prompt free users to upgrade.
Step 0: Connect Stripe first (a real prerequisite, not a one-liner)
Billing requires connecting Stripe before any plan can charge money. This is a separate onboarding step inside the dashboard (Billing → Stripe Connect) that takes roughly 10 minutes and needs a Stripe account. Until you finish it, plans you create won't be able to charge — checkout will not work. Treat this as a genuine prerequisite for the rest of this milestone, not a setting you flip in passing.
Step 1: Create a plan in the dashboard
Buildbase dashboard → Plans → New Plan.
- Name: "Pro"
- Slug:
pro - Price: you can only set a real charging price once Stripe Connect (Step 0) is done.
- Trial: optional 14-day trial
Step 2: Gate content by subscription
'use client';
import {
WhenSubscription,
WhenNoSubscription,
WhenTrialing,
WhenTrialEnding,
} from '@buildbase/sdk/react';
export default function ProFeaturePage() {
return (
<>
{/* Show trial warning */}
<WhenTrialEnding daysThreshold={3}>
<TrialEndingBanner />
</WhenTrialEnding>
{/* Show content to subscribed or trialing users */}
<WhenSubscription fallbackComponent={<UpgradePrompt />}>
<ProContent />
</WhenSubscription>
{/* Show upgrade CTA to users with no subscription */}
<WhenNoSubscription>
<UpgradePrompt />
</WhenNoSubscription>
</>
);
}Step 3: Gate by specific plan
<WhenSubscriptionToPlans
plans={['pro', 'enterprise']}
fallbackComponent={<p>This feature requires Pro or Enterprise.</p>}
>
<EnterpriseFeature />
</WhenSubscriptionToPlans>The plan slugs ('pro', 'enterprise') must match plans you created in the dashboard.
Test the checkout flow
Use the PricingPage component for a full self-serve checkout:
import { PricingPage } from '@buildbase/sdk/react';
<PricingPage slug="main-pricing" redirectBaseUrl="http://localhost:3000/dashboard">
{({ plans, selectPlan, loading }) => (
<div>
{plans.map(plan => (
<div key={plan._id}>
<h3>{plan.name}</h3>
<button onClick={() => selectPlan(plan._id, 'monthly', 'usd')}>
Subscribe
</button>
</div>
))}
</div>
)}
</PricingPage>Two silent traps here:
- `slug="main-pricing"` must match a pricing-page slug you created in the dashboard. It's not a magic value. If your
PricingPagerenders empty (no plans), the slug probably doesn't exist yet — same dashboard-first rule as feature flags. Create the pricing page in the dashboard and use its exact slug. - `plan._id` is the plan's unique ID from Buildbase (it comes from the
plansarray the component gives you). You pass it toselectPlanto say which plan the user chose.
Use Stripe test card 4242 4242 4242 4242 (any future expiry, any CVC) for testing — this only works after Stripe Connect (Step 0) is done.
What success looks like for Milestone 4
A user with no subscription sees the UpgradePrompt; a subscribed or trialing user sees ProContent. The PricingPage lists your real plans from the dashboard, and clicking Subscribe with the test card completes a checkout and flips that user into the subscribed state. If the pricing page is empty, your slug or Stripe setup isn't done yet.
Checkpoint questions for Milestone 4
1. Where does the plan slug 'pro' come from — the code or the dashboard? (Answer: the dashboard. The code only references it by name.) 2. What must be done before any plan can actually charge a user? (Answer: Stripe Connect onboarding in the dashboard, Step 0.)
Stretch questions (optional)
- What's the difference between
WhenSubscriptionandWhenSubscriptionToPlans? (Answer: the first checks for any active subscription; the second checks for a subscription to specific named plans.) - A user on a trial sees content inside
WhenSubscription— is that correct? (Answer: yes, trialing users count as having an active subscription for this gate. See billing.md if you need to treat trials separately.) - After a user subscribes, how quickly does
WhenSubscriptionreflect it? (Answer: once the SDK refreshes subscription state after checkout — see billing.md for the timing details.)
---
What's Next
After completing all four milestones, you have a working Buildbase integration with:
- Auth (sign in, session persistence, sign out)
- Content gating (auth-based)
- Feature flags (plan-based)
- Billing gates (subscription, trial, specific plans)
Recommended next steps (Builder → Advanced):
- Add usage quota tracking:
knowledge/sdk/quota-usage.md - Add server-side usage recording in API routes:
knowledge/sdk/server-side.md - Workspace switching with JWT generation (the advanced multi-workspace topic flagged earlier):
knowledge/sdk/workspace.md - Handle billing webhooks:
knowledge/sdk/server-side.md(webhook section) - Complete production wiring, every file:
knowledge/patterns/nextjs-integration.md
Key Mental Models for Buildbase
The Five Core Concepts
Understanding these five concepts is sufficient to integrate 90% of Buildbase features.
---
1. Org → Workspace → User
Org (your product/company)
└── Workspace (a team/account — the thing that gets billed)
└── User (a person with a role in this workspace)A workspace is one customer account or team — the unit that holds a subscription and usage limits. (You'll also hear it called a "tenant," which is just SaaS jargon for the same idea.)
- An org is created once in the Buildbase dashboard. It's your product.
- A workspace is created by your users. It's their team/account.
- A user belongs to one or more workspaces with a role (owner, admin, member, etc.)
- Subscriptions, quotas, features, and credits belong to workspaces, not users.
Rule: When you need to check billing, quota, or features — you need currentWorkspace._id.
---
2. The session is your login — that's all you need to start
When a user logs in, Buildbase gives you a `sessionId` (think of it as a coat-check ticket proving they're logged in). You store it in a secure cookie, and the SDK reads it back via your getSession callback. For the whole beginner path, this one token is all you need.
Advanced — you can ignore this until later. Some apps also issue their own separate token (a JWT) so their own backend API can identify the user independently of Buildbase. That's a second, optional system. You do not need it to integrate Buildbase, and you should not build it on day one. If you ever see onWorkspaceChange generating a "workspace token," that's this advanced pattern — skip it for now.| Token | What it's for | Needed to start? |
|---|---|---|
| sessionId | Proves the user is logged in to Buildbase | ✅ Yes — this is the core |
| Your own JWT | Lets your backend identify the user separately | ❌ No — advanced, optional |
---
3. Dashboard Config + SDK Code = Features
Most SDK features require configuration in the Buildbase dashboard first:
| Feature | Dashboard setup required |
|---|---|
| Auth | Configure OAuth method, get clientId |
| Plans/Billing | Create pricing plans, connect Stripe |
| Feature flags | Define features by slug |
| Quota tracking | Define quotas on plans |
| Notifications | Define events, attach email templates |
| Credits | Define credit packages |
Rule: If an SDK feature isn't working, check the dashboard first. The SDK only consumes what the dashboard configures.
---
4. Gates Return null While Loading
All gate components have three states: 1. Loading — returns null (or loadingComponent if provided) 2. Condition met — renders children 3. Condition not met — returns null (or fallbackComponent if provided)
This is by design. Gates that look "broken" are almost always in the loading state.
Debug technique: Add loadingComponent={<span>Loading...</span>} to any gate to distinguish loading from condition-not-met.
---
5. Provider Order Matters
The SaaSOSProvider wraps everything. All other SDK providers are nested inside it automatically. You don't add them manually.
// Correct: one provider at root
<SaaSOSProvider ...>
<App /> {/* WhenSubscription, useSaaSWorkspaces, etc. all work here */}
</SaaSOSProvider>
// Wrong: nested providers
<SaaSOSProvider>
<SaaSOSProvider> {/* Don't do this */}
<App />
</SaaSOSProvider>
</SaaSOSProvider>The takeaway: you only ever write one provider — SaaSOSProvider. Internally it sets up around fourteen sub-providers for you (auth, user, subscription, quota, credits, notifications, and so on), but you never touch those. One provider at the root is all your code has.
Curious about the internals? The nesting order inside SaaSOSProvider is, roughly: Translation → SDK context → FullScreenLoader → Auth → Portal → ContextConfig → CheckoutConfig → PermissionConfig → User → Subscription → QuotaUsage → CreditBalance → PushNotification → WorkspaceSettings. You don't need this to build anything — it's here only if you're debugging deep context behavior.Buildbase Core Philosophy
Skip the Plumbing, Ship the Product
Buildbase exists because auth, workspaces, billing, and notifications are infrastructure — not product differentiation. Every SaaS developer solves the same problems. Buildbase solves them once.
The cost of building these systems from scratch:
- Auth: 2–4 weeks (OAuth, sessions, email verification, password reset)
- Workspaces: 2–3 weeks (CRUD, invites, roles, permissions)
- Billing: 4–8 weeks (Stripe integration, webhooks, plan management, overage)
- Notifications: 1–2 weeks (email templates, push, preferences)
- Feature flags: 1 week
Total: 3–4 months of infrastructure work before your first line of product code.
Buildbase compresses this to hours.
---
Design Principles
1. Minimal Integration Surface
The entire client-side integration is one provider (SaaSOSProvider) + CSS import. You don't need to understand the internals to get value.
2. Server-Side by Default (for Security)
In the recommended pattern, your server stores the sessionId in an httpOnly cookie (the same approach as next-auth) and the SDK reads it back via your getSession callback — so secrets and the session stay off the client. (Note: left to its own devices the browser SDK will fall back to reading the sessionId from localStorage; the httpOnly-cookie pattern is what you implement on top to avoid that.) Your clientSecret is always server-only regardless.
3. Progressive Complexity
You can start with just authentication and add features incrementally. Subscription gates, quota tracking, and credits don't require configuration until you need them.
4. Render-Prop + Declarative Hybrid
Gate components (WhenSubscription, WhenQuotaAvailable, etc.) for simple cases. Hooks (useSubscriptionContext, useQuotaUsageContext) for complex cases. Both are always available.
5. Framework-Agnostic Server SDK
The BuildBase() factory works in Next.js, Express, Hono, Fastify, or any Node.js runtime. No framework coupling.
6. Workspaces as the Unit of Billing
Subscriptions, quotas, and credits belong to workspaces (teams/tenants), not individual users. This matches how SaaS billing works in practice.
Related skills
How it compares
Use instead of pasting the Buildbase docs into context or hoping the model remembers the SDK - the skill ships the verified API surface so the agent stops guessing.
FAQ
What is the Buildbase SDK Skill?
A Claude skill (SKILL.md + 25 knowledge files) that teaches Claude the Buildbase SaaS SDK - auth, billing, feature flags, quota, webhooks - verified against the real SDK source so it gives working code, not invented APIs.
How do I install it?
In Claude Code run /plugin marketplace add buildbase-app/claude-skill then /plugin install buildbase@buildbase-skills. You can also copy the skill folder into ~/.claude/skills, or upload the zip to claude.ai. It is open source under MIT.
Does it work outside Next.js and Node?
Yes. It is strongest on Next.js + TypeScript with a verified step-by-step path, but ships a full HTTP API reference so you can use Buildbase from Python, Go, Ruby or PHP over raw HTTP.