Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions POLAR_METER_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Polar Meter-Based Billing Setup

This guide walks through setting up usage-based billing with credits in Polar for the Pro subscription tier.

## Overview

- **Free Tier**: 10 searches per day (no meter tracking)
- **Pro Tier**: 500 search credits per month via Polar's meter-based billing

## Setup Steps

### 1. Create Usage Meter in Polar

1. Go to your Polar dashboard (sandbox or production)
2. Navigate to **Products** β†’ **Meters**
3. Click **Create Meter** with these settings:
- **Name**: `search_usage`
- **Display Name**: Search Usage
- **Event Key**: `search`
- **Aggregation Type**: Count (each search = 1 unit)

4. Copy the Meter ID and add to `.env.local`:
```
POLAR_SEARCH_METER_ID=<your-meter-id>
```

### 2. Update Product Configuration

1. In Polar dashboard, go to your Pro product
2. Edit the product and add a **Credits Benefit**:
- **Type**: Credits
- **Meter**: search_usage
- **Amount**: 500
- **Recurring**: Monthly (for subscriptions)

This will automatically grant 500 search credits at the start of each billing cycle.

### 3. Track Usage in Your Application

The application tracks usage in two ways:
- **Free users**: Daily limit tracked in Convex (`searchesUsedToday`)
- **Pro users**: Monthly credits tracked via Polar meters

When a Pro user performs a search:
1. The app reports usage to Polar's meter
2. Polar deducts from their credit balance
3. The app also tracks local usage for quick display

### 4. Monitor Customer Balance

To check a customer's remaining credits:

```typescript
// Get customer meters
const meters = await polar.customers.meters.list({
customerId: user.polarCustomerId,
});

// Find search meter balance
const searchMeter = meters.items.find(m => m.meterId === POLAR_SEARCH_METER_ID);
const remainingCredits = searchMeter?.balance || 0;
```

### 5. Handle Credit Exhaustion

The app should:
1. Check credit balance before allowing searches
2. Show remaining credits in the UI
3. Prompt users to purchase additional credits or wait for renewal

## Environment Variables

Add these to your `.env.local`:

```
# Existing Polar config
POLAR_API_KEY=your_api_key
POLAR_PRO_PRICE_ID=your_product_price_id

# New meter config
POLAR_SEARCH_METER_ID=your_meter_id
```

## Testing

1. Create a test subscription
2. Verify 500 credits are granted
3. Perform searches and verify credits decrease
4. Check that credits reset on renewal

## Notes

- Credits are granted at the start of each billing period
- Unused credits do not roll over
- Users can purchase additional credit packs if needed
- The app should gracefully handle when credits run out
41 changes: 41 additions & 0 deletions POLAR_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Polar Setup Guide

## Steps to set up Polar for Fireplexity

1. **Create a Polar account** at https://polar.sh

2. **Create an organization** if you haven't already

3. **Create a Product**:
- Go to Products β†’ Create Product
- Name: "Fireplexity Pro" (or your preferred name)
- Description: "Unlimited AI-powered searches"
- Price: $9.99 (or your preferred price)
- Billing: Monthly recurring
- Click "Create Product"

4. **Get your Product ID**:
- After creating, click on the product
- Copy the Product ID (UUID format)

5. **Get your API Key**:
- Go to Settings β†’ API Keys
- Create a new API key with "write" permissions
- Copy the API key (starts with `polar_oat_`)

6. **Update `.env.local`**:
```
POLAR_API_KEY=your_polar_api_key_here
POLAR_PRO_PRICE_ID=your_product_id_here
```

7. **Test the integration**:
- Restart your Next.js server
- Try the upgrade button in the dashboard

## Troubleshooting

- Make sure your API key has write permissions
- Ensure the Product ID matches a product in your organization
- Check that your product is active and not archived
- Verify you're using the correct environment (production vs sandbox)
54 changes: 54 additions & 0 deletions app/api/auth/callback/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { NextRequest, NextResponse } from 'next/server';
import { handleAuth } from '@workos-inc/authkit-nextjs';

const authHandler = handleAuth();

export async function GET(request: NextRequest) {
console.log('=== AUTH CALLBACK ===');
const code = request.nextUrl.searchParams.get('code');
const state = request.nextUrl.searchParams.get('state');
const error = request.nextUrl.searchParams.get('error');

console.log('Callback params:', {
hasCode: !!code,
codeLength: code?.length,
state: state ? JSON.parse(Buffer.from(state, 'base64').toString()) : null,
error
});

try {
const response = await authHandler(request);
console.log('Auth handler response:', {
status: response.status,
headers: Object.fromEntries(response.headers.entries()),
hasSetCookie: response.headers.has('set-cookie')
});

// If successful, it should redirect
if (response.status === 302 || response.status === 307) {
console.log('Redirect location:', response.headers.get('location'));
}

return response;
} catch (error) {
console.error('=== CALLBACK ERROR ===');
console.error('Error:', error);
console.error('Stack:', error instanceof Error ? error.stack : undefined);

// Return a more detailed error page
return new NextResponse(
`<html>
<body>
<h1>Authentication Error</h1>
<p>${error instanceof Error ? error.message : 'Unknown error'}</p>
<pre>${JSON.stringify({ code: !!code, state: !!state, error }, null, 2)}</pre>
<a href="/api/auth/signin">Try again</a>
</body>
</html>`,
{
status: 500,
headers: { 'content-type': 'text/html' }
}
);
}
}
33 changes: 33 additions & 0 deletions app/api/auth/clear/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
const response = NextResponse.json({ message: 'All auth cookies cleared' });

// Clear ALL cookies that might interfere with WorkOS
const cookiesToClear = [
'x-workos-session',
'wos-session',
'__session',
'__clerk_db_jwt',
'__clerk_db_jwt_X9DTgmyd',
'__clerk_db_jwt_2F6ruXmy',
'__clerk_db_jwt_4IsO9HeA',
'__clerk_db_jwt_nX2Qomuc',
'__clerk_db_jwt_WwnViksu',
'__clerk_db_jwt_kiO4uN5Z',
'sb-supabase-auth-token',
'sb-supabasekong-e4gwkcs48w80wc4os000oows-auth-token-code-verifier'
];

cookiesToClear.forEach(cookieName => {
response.cookies.set(cookieName, '', {
httpOnly: true,
secure: false,
sameSite: 'lax',
maxAge: 0,
path: '/'
});
});

return response;
}
26 changes: 26 additions & 0 deletions app/api/auth/manual-signin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from 'next/server';

export async function GET(request: NextRequest) {
const clientId = process.env.WORKOS_CLIENT_ID;
const redirectUri = process.env.WORKOS_REDIRECT_URI || 'http://localhost:3000/api/auth/callback';

if (!clientId) {
return NextResponse.json({ error: 'WORKOS_CLIENT_ID not configured' }, { status: 500 });
}

// Build the WorkOS authorization URL manually
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: redirectUri,
response_type: 'code',
provider: 'authkit',
screen_hint: 'sign-in',
state: Buffer.from(JSON.stringify({ returnPathname: '/' })).toString('base64')
});

const authUrl = `https://api.workos.com/user_management/authorize?${params.toString()}`;

console.log('Manual signin redirect:', authUrl);

return NextResponse.redirect(authUrl);
}
17 changes: 17 additions & 0 deletions app/api/auth/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from '@workos-inc/authkit-nextjs';

export async function GET(request: NextRequest) {
try {
const { user } = await withAuth();

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

return NextResponse.json({ user });
} catch (error) {
console.error('Auth check error:', error);
return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
}
}
8 changes: 8 additions & 0 deletions app/api/auth/signin/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getSignInUrl } from "@workos-inc/authkit-nextjs";
import { redirect } from "next/navigation";

export const GET = async () => {
const signInUrl = await getSignInUrl();

return redirect(signInUrl);
};
7 changes: 7 additions & 0 deletions app/api/auth/signout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { signOut } from "@workos-inc/authkit-nextjs";
import { redirect } from "next/navigation";

export const GET = async () => {
await signOut();
return redirect('/');
};
56 changes: 56 additions & 0 deletions app/api/checkout/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { NextRequest, NextResponse } from 'next/server';
import { polar } from '@/lib/polar';
import { withAuth } from '@workos-inc/authkit-nextjs';

export async function POST(request: NextRequest) {
try {
const { user } = await withAuth();

if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}

const { tier } = await request.json();

if (tier !== 'pro') {
return NextResponse.json({ error: 'Invalid subscription tier' }, { status: 400 });
}

// Check if Polar is properly configured
if (!process.env.POLAR_API_KEY || !process.env.POLAR_PRO_PRICE_ID) {
console.log('Polar not configured properly. Please follow POLAR_SETUP.md');
return NextResponse.json(
{ error: 'Payment system not configured. Please contact support.' },
{ status: 503 }
);
}

console.log('Creating checkout with:', {
priceId: process.env.POLAR_PRO_PRICE_ID,
email: user.email,
userId: user.id,
});

const checkoutSession = await polar.checkouts.create({
products: [process.env.POLAR_PRO_PRICE_ID!],
successUrl: `${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/dashboard?checkout=success`,
customerEmail: user.email,
metadata: {
workosUserId: user.id,
tier: 'pro',
},
});

console.log('Checkout session created:', checkoutSession);

return NextResponse.json({
checkoutUrl: checkoutSession.url
});
} catch (error) {
console.error('Checkout error:', error);
return NextResponse.json(
{ error: 'Failed to create checkout session. Please try again later.' },
{ status: 500 }
);
}
}
Loading