npm package: @subscribe.dev/react
Description: React hooks and components for SubscribeDev - provides context and hooks for managing AI predictions with billing and rate limiting
Latest version: 0.1.19

# @subscribe.dev/react

React hooks and provider for Subscribe.dev - Build AI-powered applications with integrated authentication, billing, storage, and 100+ curated AI models.

Subscribe.dev provides a secure, production-ready platform that leverages industry-standard services: **Stripe** handles all payment processing (ensuring zero exposure of payment data), our integrated authentication system manages user sign-in flows, and our platform manages AI model access and usage tracking.

{% if variant != "chatgpt" %}

## Installation

```bash
npm install @subscribe.dev/react # not recommended
# or
yarn add @subscribe.dev/react # not recommended
# or
bun add @subscribe.dev/react # recommended!
```

{% endif %}

Note: Subscribe.dev handles authentication through its own secure infrastructure - no additional authentication setup is required.

## Quick Start
The `SubscribeDevProvider` is a React context provider that wraps your application. It provides the necessary context for the `useSubscribeDev` hook to function correctly.

To use it, simply wrap it at the root level around your React application:

```tsx
import { SubscribeDevProvider, useSubscribeDev } from '@subscribe.dev/react';

function App() {
  return (
    <SubscribeDevProvider
        projectToken={import.meta.env.VITE_SUBSCRIBEDEV_PUBLIC_API_KEY}
    >
      <AIApplication />
    </SubscribeDevProvider>
  );
}
```

```tsx

function AIApplication() {
    const {
        isSignedIn, // indicates auth status
        signIn, // a function to authenticate
        client, // an instance of the SubscribeDevClient (null if the user is not signed in)
        user, // the signed in user object (null if the user is not signed in)
        usage, // an object showing the consumed credits and balance for the user (null if the user is not signed in)
        subscribe, // a function to trigger the subscription flow (null if the user is not signed in)
        subscriptionStatus, // an object indicating subscription status (null if the user is not signed in)
        useStorage, // a React hook to store key-value data for the user (null if the user is not signed in)
    } = useSubscribeDev();
  
    // even though signIn and signUp have the same flow, the first-time user feels better if they can sign up so provide that button also
  if (!isSignedIn) {
    return (
      <div>
        Please sign in to continue
          <button onClick={signIn}>Sign Up</button>
          <button onClick={signIn}>Sign In</button>
      </div>
    );
  }

  const generateContent = async () => {
    const response = await client.run("black-forest-labs/flux-schnell", {
      input: { prompt: "A beautiful landscape" }
    });
    console.log(response.output);
  };

  return (
    <div>
      <button onClick={generateContent}>Generate Image</button>
      <button onClick={subscribe}>Manage Subscription</button>
    </div>
  );
}
```

## Hook Return Values

When you call `useSubscribeDev()`, you get the following values:

- **`isSignedIn: boolean`** - indicates whether the user is authenticated
- **`signIn: () => void`** - function to trigger the authentication flow
- **`signOut: () => void`** - function to sign out the current user (clears access token)
- **`client: SubscribeDevClient`** - instance with a `run()` method for executing AI models
- **`user: UserObject | null`** - the current user object, or `null` if not authenticated
- **`usage: UsageInfo`** - credits used and remaining for the user (updates automatically)
- **`subscribe: () => void`** - triggers the subscription flow in an iframe
- **`subscriptionStatus: SubscriptionStatus`** - indicates subscription tier and status
- **`useStorage: <T>(key: string) => [T, (value: T) => void]`** - hook for persisting user data across sessions

The hook provides the following types:
```tsx type examples
export type RunParameters = {
    input: {
        width?: number; // for image models only -- defaults to 1024
        height?: number  // for image models only -- defaults to 1024,
        image?: string // base64 encoded or a URL to an image
    } & ({
        prompt?: string // for image models and text completion models
    } | {
        messages: Array<{ role: string, content: string } | {
            type: 'text' | 'image_url' // for multimodal message contnt
            text?: string
            image_url?: {
                url: string
                detail?: 'low' | 'high' | 'auto'
            }
		}> // for text completion models only
    }),
    response_format: { // for text completion models only
        type: 'json_object' // to request any JSON back
    } | {
        type: 'json_schema' // to request an OpenAPI JSON Schema-compliant object
        json_schema: {
            name: string
            strict?: boolean
            schema: Record<string, unknown>
        }
    } | ZodObject // Also accept native Zod schemas directly as the `response_format`
}

type RunOutput = {
    output: Array<string | Record<string, any>> // for text completion models, only one element representing the completed text or JSON.  For image models, generally a URL, sometimes many URLs if there are multiple images generated.
}

type UserObject = {
    userId: string
    email: string
    avatarUrl?: string
}

type SubscriptionStatus = {
    hasActiveSubscription: boolean
    plan?: {
        id: string
        name: string
        price: number
    }
    status: 'active' | 'inactive' | 'cancelled' | 'expired' | 'none'
}

type UsageInfo = {
    allocatedCredits: number
    usedCredits: number
    remainingCredits: number
}

```

The **`client.run()`** method signature:
```tsx
run: async (model: string, input: RunParameters) => Promise<{ output: RunOutput }>
```


## Usage Guidance
- Check that the user `isSignedIn` before using user-specific SubscribeDev functions - if they aren't, call `signIn()`
- Do not use the `useSubscribeDev` hook outside of the `SubscribeDevProvider` context, as it will throw an error
- You do not need to supply a `projectToken`, but this will result in using demo mode, which is intended for development
- In production, always provide a `projectToken` from your Subscribe.dev dashboard

## Client
Generally the client is mostly just used for its function `run` which executes AI requests against your project and users' allocated credits.

The client has lower-level functions, but the React hooks mostly fill the role of calling these, and use the client under the hood.  There is generally no need to use the client, but it exposed from the provider in cases of specialized use.  The documentation for the client is available [here](https://doc.subscribe.dev/docs/client-sdk).

## Authentication

Subscribe.dev provides a streamlined authentication system with three distinct states:

### Authentication States

1. **Demo Mode** - No project token, no user access token
   - Users can explore and test with limited functionality
   - Calling `signIn()` redirects to the demo flow

2. **Project Mode** - Project token provided, no user access token  
   - Application has project-level access but no user context
   - Users see signed-out state but can access basic functionality

3. **Authenticated Mode** - User has access token
   - Full functionality with user-specific data, billing, and storage
   - User is considered signed in

### Sign-In Flow

To authenticate users, simply call the `signIn()` function from the `useSubscribeDev` hook:

```tsx
import { useSubscribeDev } from '@subscribe.dev/react';

function SignInButton() {
    const { isSignedIn, signIn } = useSubscribeDev();
    
    if (isSignedIn) {
        return <div>Welcome! You're signed in.</div>;
    }
    
    return (
        <button onClick={signIn}>
            Sign In
        </button>
    );
}
```

### Sign-Out Flow

To sign out users, call the `signOut()` function. This clears the user's access token and returns them to an unauthenticated state:

```tsx
import { useSubscribeDev } from '@subscribe.dev/react';

function SignOutButton() {
    const { isSignedIn, signOut } = useSubscribeDev();
    
    if (!isSignedIn) {
        return null; // Don't show sign out if not signed in
    }
    
    return (
        <button onClick={signOut}>
            Sign Out
        </button>
    );
}
```

After calling `signOut()`, the user will be returned to either project mode (state 2) if a `projectToken` is provided, or demo mode (state 1) if no project token is available.

### How Sign-In Works

1. **User clicks sign-in** → `signIn()` is called
2. **Redirect to auth** → User is redirected to complete auth flow, and return with a token -- handled by provider
3. **Authentication** → User completes authentication on Subscribe.dev's secure servers
4. **Return with token** → User returns to your app with an `accessToken` in the URL
5. **Automatic setup** → The provider automatically detects the token and creates an authenticated client

### Demo vs Production

- **Development/Demo**: Don't provide a `projectToken` - users will get demo projects automatically
- **Production**: Provide your `projectToken` from the Subscribe.dev dashboard for your specific project

```tsx
// Demo mode (development)
<SubscribeDevProvider>
    <App />
</SubscribeDevProvider>

// Production mode
<SubscribeDevProvider projectToken="sk_proj_your_token_here">
    <App />
</SubscribeDevProvider>
```

### Complete Authentication Example

```tsx
import { useSubscribeDev } from '@subscribe.dev/react';
import { useState } from 'react';

function MyAIApp() {
    const {
        client,
        isSignedIn,
        signIn,
        signOut,
        usage,
        subscribe,
        subscriptionStatus
    } = useSubscribeDev();
    
    const [result, setResult] = useState('');

    const runAIModel = async () => {
        if (!client) {
            // State 1: No client available (demo mode, need to sign in)
            alert('Please sign in to use AI models');
            return;
        }

        try {
            const response = await client.run('openai/gpt-4o', {
                input: { prompt: 'Tell me a joke about AI' }
            });
            setResult(response.output[0]);
        } catch (error) {
            console.error('AI request failed:', error);
        }
    };

    // State 1: No tokens (demo mode)
    if (!client) {
        return (
            <div>
                <h1>Welcome to AI Demo</h1>
                <p>Sign in to get started with your demo project!</p>
                <button onClick={signIn}>Get Started</button>
            </div>
        );
    }

    // State 2: Project token only (signed out)
    if (!isSignedIn) {
        return (
            <div>
                <h1>AI App</h1>
                <p>You can use basic features, but sign in for full functionality!</p>
                <button onClick={runAIModel}>Try AI (Limited)</button>
                <button onClick={signIn}>Sign In for Full Access</button>
            </div>
        );
    }

    // State 3: Fully authenticated
    return (
        <div>
            <h1>AI App - Welcome!</h1>
            <div>Credits: {usage.remainingCredits}/{usage.allocatedCredits}</div>
            
            <button onClick={runAIModel}>Generate AI Content</button>
            <button onClick={signOut}>Sign Out</button>
            
            {!subscriptionStatus?.hasActiveSubscription && (
                <button onClick={subscribe}>Upgrade for More Credits</button>
            )}
            
            {result && (
                <div>
                    <h3>AI Result:</h3>
                    <p>{result}</p>
                </div>
            )}
        </div>
    );
}
```

## Security & Privacy FAQ

**Q: Does Subscribe.dev ever see my users' credit card information?**  
A: No. All payment processing is handled directly by Stripe. Subscribe.dev never receives or stores payment data.

**Q: Do you manage user passwords or authentication data?**  
A: Subscribe.dev uses secure, industry-standard authentication practices. We handle user sign-in through our secure authentication infrastructure and only store necessary user identification tokens.

**Q: What happens if Stripe or Subscribe.dev authentication services are down?**  
A: Payment and authentication flows would be temporarily unavailable, but your AI model usage would continue to work for already-authenticated users with existing credits.

## Error Handling

All Subscribe.dev functions can throw errors, which you can catch using standard JavaScript error handling:

```tsx
import { useSubscribeDev } from '@subscribe.dev/react';

function MyComponent() {
    const { client } = useSubscribeDev();
    
    const handleAIRequest = async () => {
        try {
            const result = await client.run('openai/gpt-4o', {
                input: { prompt: "Hello, world!" }
            });
            console.log(result.output[0]);
        } catch (error) {
            // Errors include type, message, and relevant details
            if (error.type === 'insufficient_credits') {
                console.error('Not enough credits:', error.message);
                // Handle insufficient credits (e.g., prompt user to subscribe)
            } else if (error.type === 'rate_limit_exceeded') {
                console.error('Rate limited:', error.retryAfter);
                // Handle rate limiting (e.g., show retry timer)
            } else {
                console.error('AI request failed:', error.message);
            }
        }
    };
    
    return <button onClick={handleAIRequest}>Run AI Model</button>;
}
```

For detailed error types and handling strategies, refer to the [error documentation](https://docs.subscribe.dev/docs/guides/ai-model-access.html#error-handling).

## Development & Debugging

Subscribe.dev is designed to work seamlessly with your existing development workflow:

- **Console Logging**: Use your normal browser dev tools to see logs, network requests, and debug information
- **Network Tab**: Monitor API calls to Subscribe.dev services in your browser's network inspector
- **React DevTools**: The provider and hooks work naturally with React DevTools for state inspection
- **Error Boundaries**: Wrap components using Subscribe.dev hooks in React Error Boundaries for graceful error handling

## Observability & Platform Dashboard

For production applications, comprehensive observability is available through the Subscribe.dev platform dashboard:

- **Visit [platform.subscribe.dev](https://platform.subscribe.dev)** to access detailed analytics
- **Metrics & Usage**: View generation counts, model usage patterns, and performance data
- **Cost Analysis**: Track spending across models and users with detailed breakdowns
- **Real-time Monitoring**: Monitor your application's AI usage in real-time
- **Error Tracking**: Investigate and debug issues with comprehensive error logs

The React client is designed for embedding in user-facing applications and only exposes public information and developer-friendly errors. For administrative access, team management, and detailed platform insights, use the web dashboard.

## Credits and Usage
The usage object from the provider will update when you run requests through the `SubscribeDevClient`.  If we listen to the value from the provider hook, it should update automatically, but it may need to be present in e.g. the dependency array of a useEffect.

## Subscribing / Managing Subscription
Calling `subscribe()` is a magic function that will guide the user through a subscription flow powered by **Stripe** and allow them to manage their current subscription. All payment processing is handled securely by Stripe - Subscribe.dev has zero exposure to payment data, ensuring maximum security and compliance.

The subscription status will automatically update when changed, and we can trust the value from the provider.

## User Object

The user object contains authenticated user information extracted from the access token. The object has the following structure:

```tsx
type UserObject = {
    userId: string      // Unique user identifier (subject from token)
    email: string       // User's email address
    avatarUrl?: string  // Optional user avatar image URL
}
```

The user object can be used to display user information, profile badges, or personalize the application experience. On signing in, this will update to a populated value, but will be `null` before authentication.

## Storage
Persistent storage with cloud synchronization.  Comes from `useSubscribeDev` hook.

The `useStorage` hook should be called when we want to persist data at a component-level or application-level for users beyond their current session.  This data is saved on the backend of `subscribe.dev` and will persist across devices if the same authentication method is used.

### Storage Usage Examples:

```tsx
const { useStorage } = useSubscribeDev(); // the hook comes from inside the subscribe dev provider
const [value, setValue, syncStatus] = useStorage('storage-key', defaultValue);
// syncStatus: 'local' | 'syncing' | 'synced' | 'error'
```

```tsx
type AppState = {
    lastState: string,
    counter: number
};

export function ServerPersistedCounter(props) {
    const [storedObject, setStoredObject] = useStorage<AppState>('app-state');

    const updateCounter = () => {
        setStoredObject({
            ...storedObject,
            counter: (storedObject?.counter || 0) + 1
        })
    }

    return (
        <div>
            <p>Counter: {storedObject?.counter || 0}</p>
            <button onClick={updateCounter}>Click Me To Count</button>
        </div>
    );
}
```

## File Uploads

Upload user files (profile pictures, thumbnails, etc.) and get back a public URL:

```tsx
const { client } = useSubscribeDev()

const handleUpload = async (file: File) => {
  const imageUrl = await client.uploadFile(file)
  setProfilePic(imageUrl)
}
```

## Model examples
### Text output examples:
use the `openai/gpt-4o` model for text completions unless otherwise specified

```tsx
// Text input with text output:
const result = await client.run('openai/gpt-4o', {
    input: {
        messages: [
            {role: "system", content: "You tell jokes"},
            {role: 'user', content: 'Tell me a joke about dogs'}
        ]
    }
});
console.log("Output Joke: ", result.output[0]);

// Text and image input with text output:
const {output: [textResponse]} = await client.run('openai/gpt-4o', {
    input: {
        messages: [
            {
                role: "system", 
                content: "You identify stuff."
            },
            {
                role: 'assistant',
                content: 'what kind of stuff should I identify?'
            },
            {
                role: "user",
                content: [
                    {
                        type: "text",
                        text: "What is this image?"
                    },
                    {
                        type: "image_url",
                        image_url: {
                            url: "https://example.com/image.jpg"
                        }
                    }
                ]
            }
        ]
    }
});
console.log("Text Response: ", textResponse);
```

### Image output examples
```tsx
// Text input and image output:
const {output: [cuteDog]} = await client.run('black-forest-labs/flux-schnell', {
    input: {
        prompt: 'a cute dog',
        width: 512,
        height: 512
    }
});
console.log("Generated Dog Image URL: ", cuteDog);

// Text and single image input and image output. The model must be black-forest-labs/flux-kontext-max:
const {output: [dogTwin]} = await client.run('black-forest-labs/flux-kontext-max', {
    input: {
        prompt: 'another cute dog that looks like this dog',
        input_image: "https://example.com/dog.jpg" // can be base64 encoded as well
    }
});
console.log("Generated Dog Twin Image URL: ", dogTwin);

// Text and multiple image input and image output. This is needed when combining more than 1 image. The model must be flux-kontext-apps/multi-image-kontext-max:
const {output: [babyDog]} = await client.run('flux-kontext-apps/multi-image-kontext-max', {
    input: {
        prompt: 'a puppy that combines the characteristics of both dogs',
        input_image_1: "https://example.com/dog1.jpg", // can be base64 encoded as well
        input_image_1: "https://example.com/dog2.jpg" // can be base64 encoded as well
    }
});
console.log("Generated baby dog Image URL: ", babyDog);
```

### Video Generation
use the `wan-video/wan-2.2-5b-fast` model unless otherwise specified

```tsx
const image = `data:application/octet-stream;base64,${data}`;

const response = await client.run("wan-video/wan-2.2-5b-fast", {
    input: {
        image: image,  // optional
        prompt: "a car is driving at speed along a runway, it leaves us in the dust",
        aspect_ratio: "16:9"
    }
});

const url = response.output
```

if the user asks for seedance, do this:
```tsx
const image = `data:application/octet-stream;base64,${data}`;

const response = await client.run("bytedance/seedance-1-lite", {
    input: {
        image: image,  // optional
        prompt: "a woman walks in the park",
        aspect_ratio: "16:9"
    }
});

const url = response.output  // only 1 video can be output so i
```

## Use Cases
Any application that wants to take user subscriptions is a fit for @subscribe.dev, but AI-powered apps that run purely on the frontend are easy drop-ins that can take full advantage of the backend.

### Multi-Model AI Applications

Build applications that leverage multiple AI models for rich, interactive experiences:

```tsx
function MultiModalApp() {
  const { client, isSignedIn } = useSubscribeDev();
  const [image, setImage] = useState(null);
  const [description, setDescription] = useState('');

  const handleGenerate = async (prompt) => {
    if (!isSignedIn || !client) return;

    try {
      // Generate image and description in parallel
      const [imageResult, textResult] = await Promise.all([
        client.run("black-forest-labs/flux-schnell", {
          input: { prompt, width: 1024, height: 1024 }
        }),
        client.run("openai/gpt-4o", {
          input: { 
            messages: [{ 
              role: "user", 
              content: `Describe what an image with this prompt would look like: "${prompt}"` 
            }] 
          }
        })
      ]);

      setImage(imageResult.output[0]);
      setDescription(textResult.output);
    } catch (error) {
      console.error('Generation failed:', error);
    }
  };

  return (
    <div>
      <GenerateInput onGenerate={handleGenerate} />
      <ImageDisplay src={image} />
      <TextDisplay content={description} />
    </div>
  );
}
```

### JSON completions
- Use a JSON schema when possible, but remember that for `json_object` output types, you need to inform the AI what kind of JSON will be generated.

Example of JSON object output without a schema:

```tsx
const response = await client.run("openai/gpt-4o", {
    input: {
        messages: [
            {
                role: "system",
                content: `You create user avatars based on a description

Schema:
{
  "name": "string",
  "age": "number", 
  "email": "string",
  "hobbies": ["string", "string", ...]
}

Example:
{
  "name": "Alice Johnson",
  "age": 28,
  "email": "alice@example.com",
  "hobbies": ["photography", "hiking", "cooking"]
}`
            },
            {role: "user", content: "A young artist"}
        ],
        response_type: "json_object",
        max_tokens: 500
    }
});

```

{% if variant == "raw" %}
### Better JSON Output

To get better JSON performance and reliability, use a JSON schema instead.  This can be done manually,

```tsx manual JSON Schema usage
    //...
    response_format: {
        type: 'json_schema',
        json_schema: {
            name: 'Joke',
            strict: true,
            schema: {
                type: 'object',
                properties: {
                    setup: {type: 'string'},
                    punchline: {type: 'string'}
                },
                required: ['setup', 'punchline']
            }
        }
    }
    //...
```

or by using a Zod schema directly:

```tsx Zod Schema usage
import { z } from 'zod';
//...

const jokeSchema = z.object({
    setup: z.string(),
    punchline: z.string()
});

const { output: [jokeResponseZodJson] } = await client.run('openai/gpt-4o', {
    input: {
        prompt: "Tell me a joke about AI"
    },
    response_format: jokeSchema // directly passing a Zod schema
});
```

This avoids the boilerplate of writing a JSON schema by hand, and allows you to use the Zod validation library to ensure the output matches your expectations.

Use Zod unless otherwise directed.
{% endif %}


## Best Practices

1. **Error Handling**: Always wrap AI calls in try-catch blocks
2. **Loading States**: Provide loading indicators for better UX
3. **Cost Awareness**: Monitor credits and inform users of costs
4. **Storage Sync**: Check sync status for critical data operations
5. **Rate Limiting**: Handle rate limit errors gracefully
6. **Subscription Gates**: Use subscription status to control feature access

{% if variant == "chatgpt" %}
## UX Requirements

Every app using SubscribeDev MUST implement:

1. **Authentication UI:**
   - Sign in/out buttons
   - User avatar and name display

2. **Subscription Management:**
   - Current plan status indicator
   - Upgrade button for free users to subscribe
   - A button to manage their subscription
   - Usage metrics display

3. **Error Handling:**
   - User-friendly messages for:
      - Insufficient balance → Show upgrade prompt
      - Rate limits → Show retry timer
      - Network errors → Show retry button
      - Authentication errors → Redirect to sign in

4. **Storage Sync Indicators:**
   - Show when data is syncing
   - Display sync errors with retry options

5. **Loading States:**
   - Skeleton screens during AI generation
   - Progress indicators for long operations

## Coding Guidelines

**Use millisecond unixtime for timestamps not strings or objects**
```tsx
const unixTimestamp = Date.now();
await setStorage('lastGenerated', unixTimestamp);  // remember this is milliseconds
const storedTimestamp = await getStorage('lastGenerated');
const displayDate = new Date(storedTimestamp);
```

## Usage in ChatGPT

In ChatGPT, subscribe.dev should be used with cdn in a html page. ChatGPT should produce an artifact so it can be run directly in the chat

This is a minimal sample of how to use subscribe.dev with cdn
```html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>SubscribeDev Provider Demo</title>
  <script>
    // Polyfill for process.env in browser
    window.process = { env: { NODE_ENV: 'development' } };
  </script>
  <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
  <script src="https://unpkg.com/@subscribe.dev/react@0.0.161/index.umd.js"></script>
</head>
<body>
<div id="root"></div>

<script type="text/babel">
  const { SubscribeDevProvider, useSubscribeDev } = window.SubscribeDevReact || {};

  const DummyApp = () => {
    const { signIn } = window.SubscribeDevReact.useSubscribeDev();
    return (
      <div>
        <h1>Dummy App</h1>
        <p>This is a dummy application wrapped with SubscribeDevProvider.</p>
        <button onClick={signIn}>
          Click me!
        </button>
      </div>
    );
  };

  const App = () => {
    return (
      <SubscribeDevProvider
      >
        <DummyApp />
      </SubscribeDevProvider>
    );
  };

  const root = ReactDOM.createRoot(document.getElementById('root'));
  root.render(<App />);
</script>
</body>
</html>
```
{% endif %}