Stop Reinventing the Onboarding Wheel. Start Building.

Your users need smooth flows. You need clean code, faster. OnboardJS handles the messy state stuff so that you can focus on building.

WelcomeStep.tsx
stepsConfig.ts
function WelcomeStep() {
const { next } = useOnboarding()
return (
<div>
<button onClick={next}>Next</button>
</div>
)
}

Plugins & Integrations

Supabase Persistence Plugin

The easiest way to persist your OnboardJS flow data directly to your Supabase backend, ensuring users never lose progress and their onboarding journey is always seamless.


Why Supabase for Onboarding Persistence?

Did you know that every time a user refreshes the page or switches devices, they could lose their onboarding progress if it's not properly persisted? This leads to frustration, drop-offs, and missed opportunities.

Supabase offers a powerful, PostgreSQL-backed solution that's perfect for storing dynamic user data like onboarding flow state. With real-time capabilities and robust authentication, it's a natural fit for building personalized, durable user experiences.

The OnboardJS Supabase plugin handles all the tedious plumbing, so you can focus on designing great user journeys, not database operations.

Key Benefits

  • Seamless User Experience: Users can close their browser or switch devices and resume exactly where they left off.
  • Reliable Data Storage: Leverage PostgreSQL's stability and Supabase's ease of use for your flow data.
  • User-Specific State: Automatically tie onboarding progress to your authenticated Supabase users.
  • Analytics & Personalization: Collect rich data on user journeys to inform future product decisions and tailor experiences.
  • Reduced Boilerplate: No more writing custom loadData, persistData, or clearData functions for your backend.

Installation

First, install the plugin package:

bash
3 lines
1npm install @onboardjs/supabase-plugin
2# or
3yarn add @onboardjs/supabase-plugin

Setup & Usage

The Supabase Persistence Plugin integrates directly with your OnboardingProvider in @onboardjs/react or directly with the OnboardingEngine in @onboardjs/core.

Before you start:

  1. Supabase Client: Ensure you have a Supabase client instance initialized in your application.
  2. Database Table: Create a table in your Supabase project to store the onboarding state. A basic schema might look like this:
sql
22 lines
1CREATE TABLE onboarding_state (
2  user_id UUID PRIMARY KEY REFERENCES auth.users(id),
3  flow_data JSONB, -- Stores the JSON representation of your OnboardingContext
4  created_at TIMESTANDZ DEFAULT NOW(),
5  updated_at TIMESTANDZ DEFAULT NOW()
6);
7
8-- Optional: RLS policies to allow users to read/write their own state
9-- For authenticated users:
10ALTER TABLE onboarding_state ENABLE ROW LEVEL SECURITY;
11CREATE POLICY "Users can view their own onboarding state."
12  ON onboarding_state FOR SELECT
13  TO authenticated
14  USING (auth.uid() = user_id);
15CREATE POLICY "Users can insert their own onboarding state."
16  ON onboarding_state FOR INSERT
17  TO authenticated
18  WITH CHECK (auth.uid() = user_id);
19CREATE POLICY "Users can update their own onboarding state."
20  ON onboarding_state FOR UPDATE
21  TO authenticated
22  USING (auth.uid() = user_id);

1. Configure the Plugin

The plugin's configuration options allow you to tailor it to your Supabase setup and how you manage user IDs.

tsx
16 lines
1import { createSupabasePlugin } from '@onboardjs/supabase-plugin'
2
3// Example usage:
4const supabaseClient = createClient() // Your Supabase client instance
5
6const supabasePlugin = createSupabasePlugin<YourAppContext>({
7  client: supabaseClient,
8  tableName: 'onboarding_progress', // Matches your table name
9  userIdColumn: 'user_id', // Matches your user ID column
10  stateDataColumn: 'flow_data', // Matches your state data column
11  useSupabaseAuth: true, // Recommended: Automatically link to Supabase authenticated user
12  onError: (error, operation) => {
13    console.error(`[SupabasePlugin] Error during ${operation}:`, error.message)
14    // You might want to send this to a dedicated error tracking service
15  },
16})

2. Add to Your OnboardingProvider (React)

Once configured, pass the supabasePlugin instance to the plugins array of your OnboardingProvider.

tsx
67 lines
1// src/components/OnboardingProviderWrapper.tsx
2'use client' // Important for Next.js App Router
3
4import React from 'react'
5import { OnboardingProvider } from '@onboardjs/react'
6import { createSupabasePlugin } from '@onboardjs/supabase-plugin'
7import { createClient } from '@/lib/supabase' // Your Supabase client setup
8import { type User } from '@supabase/auth-js' // Supabase user type
9import { OnboardingContext } from '@onboardjs/core' // Core OnboardingContext
10
11// Define your App's custom OnboardingContext
12interface AppOnboardingContext extends OnboardingContext {
13  currentUser?: User // This field will be populated by the plugin if `useSupabaseAuth: true`
14  // ... other application-specific context properties
15}
16
17// Your common flow steps and component registry would be imported here
18// import { commonFlowSteps, commonRegistry } from "./common-flow-config";
19
20export default function OnboardingProviderWrapper({
21  user, // (Optional) Pass the Supabase authenticated user from your auth context/layout
22  children,
23}: Readonly<{
24  user: User | null
25  children: React.ReactNode
26}>) {
27  const client = createClient() // Initialize your Supabase client
28
29  // Create the Supabase plugin instance
30  const supabasePlugin = createSupabasePlugin<AppOnboardingContext>({
31    client,
32    tableName: 'onboarding_progress',
33    userIdColumn: 'user_id',
34    stateDataColumn: 'flow_data',
35    useSupabaseAuth: true, // Crucial: plugin will fetch and use auth.getUser().id
36    onError(error, operation) {
37      console.error(
38        `[SupabasePlugin] Error during ${operation}:`,
39        error.message,
40      )
41    },
42  })
43
44  // Pass the Supabase user to the initial context if available.
45  // The plugin will ensure `context.currentUser` is set from Supabase Auth data.
46  // This helps when the engine first loads, ensuring `currentUser` is present.
47  const initialContextWithUser: Partial<AppOnboardingContext> = {
48    // Other initial flowData or context you need
49    flowData: {
50      selectedOption: 'default-flow',
51    },
52    // Initialize currentUser from external props, if available.
53    // The plugin will overwrite/confirm this with fetched Supabase auth data.
54    currentUser: user ?? undefined,
55  }
56
57  return (
58    <OnboardingProvider<AppOnboardingContext>
59      initialContext={initialContextWithUser}
60      steps={commonFlowSteps} // Your defined onboarding steps (replace commonFlowSteps)
61      plugins={[supabasePlugin]} // Crucial: Add the plugin here
62      componentRegistry={commonRegistry} // Your React component mapping for steps (replace commonRegistry)
63    >
64      {children}
65    </OnboardingProvider>
66  )
67}

3. Using with @onboardjs/core (Headless)

If you're using the core OnboardingEngine directly (e.g., in a Node.js backend or a different frontend framework), you can install the plugin directly:

tsx
37 lines
1import { OnboardingEngine, OnboardingContext } from '@onboardjs/core'
2import { createSupabasePlugin } from '@onboardjs/supabase-plugin'
3import { createClient, User } from '@supabase/supabase-js' // Your Supabase client setup
4
5// Define your App's custom OnboardingContext
6interface AppOnboardingContext extends OnboardingContext {
7  currentUser?: User
8  // ...
9}
10
11const supabaseClient = createClient(
12  process.env.NEXT_PUBLIC_SUPABASE_URL!,
13  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
14)
15
16const engine = new OnboardingEngine<AppOnboardingContext>({
17  steps: [], // Your onboarding steps
18  initialContext: {
19    // You must provide `currentUser` here if not using `useSupabaseAuth: true`
20    // OR ensure it gets updated into the context before persistence operations are called.
21  },
22  // You would typically not define loadData/persistData here if using the plugin
23})
24
25// Install the plugin after engine creation
26engine.use(
27  createSupabasePlugin<AppOnboardingContext>({
28    client: supabaseClient,
29    tableName: 'onboarding_progress',
30    userIdColumn: 'user_id',
31    stateDataColumn: 'flow_data',
32    useSupabaseAuth: true,
33  }),
34)
35
36// Don't forget to await engine.ready() before interacting with it
37// await engine.ready();

How the Plugin Handles User IDs

The plugin needs a way to identify which user's onboarding state to load or persist. It offers two primary methods:

By setting useSupabaseAuth: true, the plugin will automatically:

  1. On engine initialization, call supabase.auth.getUser() to retrieve the currently authenticated user.
  2. Use that user's id for all database queries (SELECT, UPSERT, UPDATE).
  3. Automatically set context.currentUser = user in your OnboardingContext, ensuring your application has access to the user object throughout the flow.

This is the most seamless way to integrate if your application relies on Supabase for authentication.

Method 2: contextKeyForId (Manual)

If useSupabaseAuth is false, you must provide contextKeyForId. This tells the plugin which path in your OnboardingContext holds the user's unique ID.

Example:

  1. If your OnboardingContext looks like { flowData: {}, userProfile: { userId: '123' } }, you would set contextKeyForId: 'userProfile.userId'.
  2. If your OnboardingContext looks like { flowData: {}, id: '123' }, you would set contextKeyForId: 'id'.

Important: Ensure the value at contextKeyForId is a string and is present in your initialContext or is updated into the context before any persistence operations (like next(), updateContext(), reset()) trigger a save. If the ID is missing, persistence operations will be skipped.

Database Schema Recommendation

For optimal use, your Supabase table should at least have:

  • A user_id column (e.g., UUID) which links to auth.users(id) if using Supabase Auth.
  • A flow_data column (e.g., JSONB) to store the entire OnboardingContext (excluding functions).
sql
21 lines
1CREATE TABLE onboarding_progress (
2  id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
3  user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE,
4  flow_data JSONB,
5  created_at TIMESTAMPTZ DEFAULT NOW(),
6  updated_at TIMESTAMPTZ DEFAULT NOW()
7);
8
9-- For Next.js/Frontend access via Row Level Security (RLS)
10ALTER TABLE onboarding_progress ENABLE ROW LEVEL SECURITY;
11
12CREATE POLICY "Allow authenticated users to read their own onboarding progress"
13  ON onboarding_progress FOR SELECT
14  TO authenticated
15  USING (auth.uid() = user_id);
16
17CREATE POLICY "Allow authenticated users to insert/update their own onboarding progress"
18  ON onboarding_progress FOR INSERT WITH CHECK (auth.uid() = user_id);
19
20CREATE POLICY "Allow authenticated users to update their own onboarding progress"
21  ON onboarding_progress FOR UPDATE USING (auth.uid() = user_id);

Why JSONB for flow_data? JSONB is PostgreSQL's optimized binary JSON type. It's efficient for storage and allows for indexing and querying within the JSON data if needed (though not directly used by this plugin for internal queries).

Error Handling

The plugin includes an onError callback in its configuration. This is highly recommended for debugging and monitoring persistence issues:

tsx
11 lines
1createSupabasePlugin({
2  // ... config
3  onError: (error, operation) => {
4    console.error(
5      `[Supabase Plugin] Failed to ${operation} onboarding state:`,
6      error.message,
7      error.details, // Supabase specific error details
8    )
9    // Integrate with Sentry, LogRocket, or your preferred error tracking system
10  },
11})

If onError is not provided, or if the error is re-thrown by your custom onError handler, the error will be propagated to the main OnboardingEngine's global error handler.

Best Practices with the Supabase Plugin

  • Secure Your Data: Always implement Row Level Security (RLS) on your onboarding_state table to ensure users can only access their own data.
  • Keep Context Lean: Only persist necessary data in your OnboardingContext. Avoid functions, large binary objects, or transient UI state.
  • Monitor Errors: Pay attention to errors reported by the onError callback to quickly identify and fix persistence issues.
  • Clear Completed Flows: Consider clearing a user's onboarding state from Supabase once their flow is isCompleted to keep your database clean. The plugin's clearPersistedData function can be triggered via engine.reset() or if you explicitly call it.

Troubleshooting

  • "Persistence Not Working":
    • Is userId resolving? Ensure the plugin can correctly obtain the user ID, either via useSupabaseAuth: true (and a logged-in user) or correctly configured contextKeyForId with the ID present in context. Check console warnings.
    • Check Supabase RLS: Verify your RLS policies are not preventing SELECT, INSERT, or UPDATE operations. Test with a Supabase service role key (for debugging only) to rule out RLS issues.
    • Console Errors: Look for any errors logged by the onError callback or the main OnboardJS engine.
  • "Corrupted Data / Invalid State": If the data loaded from flow_data causes issues (e.g., due to schema changes), you might need to implement a data migration strategy on load, or clear the corrupted state for the user.

Contributing

This plugin is open-source and contributions are welcome! If you have ideas for improvements, new features, or encounter any issues, please check the plugin's GitHub repository.

Previous
Overview