Skip to content
Closed
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
2 changes: 1 addition & 1 deletion convex/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function hasProAccess(
}

/**
* Legacy compatibility: Get user ID (now just returns Better Auth user ID)
* Legacy compatibility: Get user ID (alias for Stack Auth identifiers)
* @deprecated Use getCurrentUserId instead
*/
export async function getCurrentUserClerkId(
Expand Down
14 changes: 7 additions & 7 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export default defineSchema({
// Projects table
projects: defineTable({
name: v.string(),
userId: v.string(), // Better Auth user ID (not v.id - we'll store the Better Auth ID directly)
userId: v.string(), // Stack Auth user ID (not v.id - we store the Stack Auth ID directly)
framework: frameworkEnum,
modelPreference: v.optional(v.string()), // User's preferred AI model (e.g., "auto", "anthropic/claude-haiku-4.5", "openai/gpt-4o")
createdAt: v.optional(v.number()), // timestamp
Expand Down Expand Up @@ -135,7 +135,7 @@ export default defineSchema({

// OAuth Connections table - for storing encrypted OAuth tokens
oauthConnections: defineTable({
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
provider: oauthProviderEnum,
accessToken: v.string(), // Encrypted token
refreshToken: v.optional(v.string()),
Expand All @@ -150,7 +150,7 @@ export default defineSchema({

// Imports table - tracking import history and status
imports: defineTable({
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
projectId: v.id("projects"),
messageId: v.optional(v.id("messages")),
source: importSourceEnum,
Expand All @@ -169,7 +169,7 @@ export default defineSchema({

// Usage table - rate limiting and credit tracking
usage: defineTable({
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
points: v.number(), // Remaining credits
expire: v.optional(v.number()), // Expiration timestamp
planType: v.optional(v.union(v.literal("free"), v.literal("pro"))), // Track plan type
Expand All @@ -190,7 +190,7 @@ export default defineSchema({

// Subscriptions table - Polar.sh subscription tracking
subscriptions: defineTable({
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
polarCustomerId: v.string(), // Polar.sh customer ID
polarSubscriptionId: v.string(), // Polar.sh subscription ID
productId: v.string(), // Polar product ID
Expand Down Expand Up @@ -218,7 +218,7 @@ export default defineSchema({
sandboxSessions: defineTable({
sandboxId: v.string(), // E2B sandbox ID
projectId: v.id("projects"), // Associated project
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
framework: frameworkEnum, // Framework for the sandbox
state: sandboxStateEnum, // RUNNING, PAUSED, or KILLED
lastActivity: v.number(), // Timestamp of last user activity
Expand Down Expand Up @@ -268,7 +268,7 @@ export default defineSchema({
jobQueue: defineTable({
type: v.string(), // Job type: "code_generation", "error_fix", etc.
projectId: v.id("projects"),
userId: v.string(), // Better Auth user ID
userId: v.string(), // Stack Auth user ID
payload: v.any(), // Job-specific data (event.data from Inngest)
priority: v.union(v.literal("high"), v.literal("normal"), v.literal("low")),
status: v.union(
Expand Down
5 changes: 4 additions & 1 deletion src/app/.well-known/openid-configuration/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server";

export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_BETTER_AUTH_URL || "http://localhost:3000";
const baseUrl =
process.env.NEXT_PUBLIC_APP_URL ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:3000";
return NextResponse.json({
issuer: baseUrl,
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
Expand Down
139 changes: 118 additions & 21 deletions src/app/api/polar/create-checkout/route.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,131 @@
import { NextRequest, NextResponse } from "next/server";

import { getUser } from "@/lib/stack-auth";
import {
getPolarClient,
getPolarOrganizationId,
getPolarProProductId,
isPolarConfigured,
} from "@/lib/polar-client";
import { getSanitizedErrorDetails, validatePolarEnv } from "@/lib/env-validation";

type CheckoutRequest = {
productId?: string;
successUrl?: string;
cancelUrl?: string;
};

function getBaseUrl(): string {
return (
process.env.NEXT_PUBLIC_APP_URL ||
process.env.NEXT_PUBLIC_BASE_URL ||
"http://localhost:3000"
);
}

function buildResponse(
status: number,
payload: {
error: string;
details?: string;
isConfigError?: boolean;
adminMessage?: string;
},
) {
return NextResponse.json(payload, { status });
}

// NOTE: Polar checkout will be implemented after Stack Auth is fully configured
// This is a placeholder route for now
export async function POST(req: NextRequest) {
try {
// Authenticate user with Stack Auth
const user = await getUser();

if (!user) {
return NextResponse.json(
{ error: "Unauthorized - Please sign in to continue" },
{ status: 401 }
);
return buildResponse(401, {
error: "Unauthorized",
details: "Please sign in to continue",
});
}

// TODO: Implement Polar checkout once Stack Auth is configured with proper API keys
return NextResponse.json(
{ error: "Polar checkout not yet configured. Please set up Stack Auth first." },
{ status: 501 }
);
if (!isPolarConfigured()) {
return buildResponse(503, {
error: "Payment system is not configured",
details: "Please contact support while we finish setting up billing.",
isConfigError: true,
adminMessage: "Missing Polar environment variables. Run validatePolarEnv() for details.",
});
}

const body = (await req.json().catch(() => ({}))) as CheckoutRequest;
const requestedProductId = body.productId?.trim();

let productId = requestedProductId ?? "";
if (!productId) {
try {
productId = getPolarProProductId();
} catch {
return buildResponse(503, {
error: "Polar product is not configured",
details: "Set NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID to your Polar product ID.",
isConfigError: true,
adminMessage: "NEXT_PUBLIC_POLAR_PRO_PRODUCT_ID is missing",
});
}
}
if (!productId) {
return buildResponse(500, {
error: "Unable to determine Polar product",
details: "Product ID resolution failed unexpectedly.",
adminMessage: "Polar product ID empty after configuration check",
});
}

validatePolarEnv(true);
const polar = getPolarClient();

const baseUrl = getBaseUrl();
const successUrl =
body.successUrl || `${baseUrl}/dashboard/subscription?status=success`;
const cancelUrl =
body.cancelUrl || `${baseUrl}/dashboard/subscription?status=cancelled`;

const checkout = await polar.checkoutSessions.create({
organizationId: getPolarOrganizationId(),
productPriceId: productId,
successUrl,
cancelUrl,
customerEmail: user.primaryEmail ?? undefined,
customerName: user.name ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
Comment on lines +95 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Fix Stack Auth user field: use displayName instead of name

CurrentServerUser (the return type of stackServerApp.getUser()) inherits from CurrentUser, which exposes displayName, not name. (docs.stack-auth.com) This causes the Property 'name' does not exist on type 'CurrentServerUser' error.

Change the customer name mapping to use displayName:

-        customerName: user.name ?? undefined,
+        customerName: user.displayName ?? undefined,

🌐 Web query:

In the latest Stack Auth docs, which field on `CurrentServerUser` should be used as the human-readable name: `name` or `displayName`?

💡 Result:

Use displayName — CurrentServerUser inherits CurrentUser which exposes displayName (not name). [1][2]

Sources:
[1] Stack Auth docs — CurrentServerUser / CurrentUser type reference.
[2] Stack Auth docs (next) — CurrentUser.displayName description.


Fix Stack Auth user field: use displayName instead of name

CurrentServerUser (the return type of stackServerApp.getUser()) inherits from CurrentUser, which exposes displayName, not name. This causes the Property 'name' does not exist on type 'CurrentServerUser' error.

Change the customer name mapping to use displayName:

-        customerName: user.name ?? undefined,
+        customerName: user.displayName ?? undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
customerEmail: user.primaryEmail ?? undefined,
customerName: user.name ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
customerEmail: user.primaryEmail ?? undefined,
customerName: user.displayName ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
🧰 Tools
🪛 GitHub Actions: CI

[error] 96-96: TS2339: Property 'name' does not exist on type 'CurrentServerUser'.

🤖 Prompt for AI Agents
In src/app/api/polar/create-checkout/route.ts around lines 95 to 99, the code
references user.name which does not exist on CurrentServerUser; replace that
usage with user.displayName (preserving the existing nullish fallback to
undefined) so customerName: user.displayName ?? undefined. Ensure any other
occurrences in that block or metadata mapping use displayName instead of name.

},
});
Comment on lines +90 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Use polar.checkouts.create instead of checkoutSessions.create

The CI error stems from calling polar.checkoutSessions.create on the Polar client; the current SDK exposes the checkout API under polar.checkouts.create(...), not checkoutSessions. (polar.sh)

Also, the Checkout API expects product IDs via products: string[] rather than a single productPriceId.

Consider updating this block along these lines:

-      const checkout = await polar.checkoutSessions.create({
-        organizationId: getPolarOrganizationId(),
-        productPriceId: productId,
-        successUrl,
-        cancelUrl,
-        customerEmail: user.primaryEmail ?? undefined,
-        customerName: user.name ?? undefined,
-        metadata: {
-          userId: user.id,
-          userEmail: user.primaryEmail ?? undefined,
-        },
-      });
+      const checkout = await polar.checkouts.create({
+        products: [productId],
+        successUrl,
+        cancelUrl,
+        customerEmail: user.primaryEmail ?? undefined,
+        customerName: user.displayName ?? undefined,
+        metadata: {
+          userId: user.id,
+          userEmail: user.primaryEmail ?? undefined,
+        },
+      });

Please verify the exact option names against the installed @polar-sh/sdk version and add any additional fields you need (e.g. externalCustomerId) to align with your webhook mapping.


🌐 Web query:

What is the current TypeScript signature for `checkouts.create` in `@polar-sh/sdk`, including how to pass product IDs, redirect URLs, and customer metadata?

💡 Result:

Short answer — signature and how to pass fields

TypeScript signature (shape used by @polar-sh/sdk — camelCased by the TS SDK):
const checkout = await polar.checkouts.create({
products: string[], // required — list of product IDs (first is default)
successUrl?: string, // optional — redirect after successful payment
returnUrl?: string, // optional — back button return URL
metadata?: Record<string, string|number>, // optional — checkout metadata copied to Order/Subscription
customerMetadata?: Record<string, string|number|boolean|number>, // optional — copied to created Customer
externalCustomerId?: string, // optional — your own customer id (external_customer_id)
trialInterval?: 'day'|'week'|'month'|'year'|null,
trialIntervalCount?: number|null,
embedOrigin?: string|null,
// ...other optional fields described in API
});
(Fields are camelCased by the TS SDK; the underlying API uses snake_case.)

Example minimal usage:
import { Polar } from "@polar-sh/sdk";
const polar = new Polar({ accessToken: process.env.POLAR_ACCESS_TOKEN ?? "" });
const checkout = await polar.checkouts.create({
products: ["prod_abc123", "prod_def456"],
successUrl: "https://example.com/thank-you?checkout_id={CHECKOUT_ID}",
returnUrl: "https://example.com/pricing",
metadata: { campaign: "black-friday" },
customerMetadata: { accountId: "acct_987" },
});

Sources: Polar docs — Create Checkout Session and TypeScript SDK (camelCase note). [1][2]

References
[1] Create Checkout Session (checkouts/create) — Polar docs.
[2] TypeScript SDK — Polar docs (camelCase conversion note).


Use polar.checkouts.create instead of checkoutSessions.create and correct the redirect URL parameter

The current call to polar.checkoutSessions.create does not exist in @polar-sh/sdk; use polar.checkouts.create instead. The API expects product IDs via products: string[] (not productPriceId), but the suggested diff contains an error: the parameter is returnUrl (not cancelUrl). Also, organizationId is not part of the new API signature.

The Checkout API also provides customerMetadata (for customer-related data) separate from metadata (for order/subscription data), which may be more appropriate for your user fields.

Update the code:

-      const checkout = await polar.checkoutSessions.create({
-        organizationId: getPolarOrganizationId(),
-        productPriceId: productId,
+      const checkout = await polar.checkouts.create({
+        products: [productId],
         successUrl,
-        cancelUrl,
+        returnUrl,
         customerEmail: user.primaryEmail ?? undefined,
-        customerName: user.name ?? undefined,
+        customerName: user.displayName ?? undefined,
         metadata: {
           userId: user.id,
           userEmail: user.primaryEmail ?? undefined,
         },
       });

Verify that user.displayName is the correct property on your User object (the original used user.name).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const checkout = await polar.checkoutSessions.create({
organizationId: getPolarOrganizationId(),
productPriceId: productId,
successUrl,
cancelUrl,
customerEmail: user.primaryEmail ?? undefined,
customerName: user.name ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
},
});
const checkout = await polar.checkouts.create({
products: [productId],
successUrl,
returnUrl,
customerEmail: user.primaryEmail ?? undefined,
customerName: user.displayName ?? undefined,
metadata: {
userId: user.id,
userEmail: user.primaryEmail ?? undefined,
},
});
🧰 Tools
🪛 GitHub Actions: CI

[error] 90-90: TS2339: Property 'checkoutSessions' does not exist on type 'Polar'.


[error] 96-96: TS2339: Property 'name' does not exist on type 'CurrentServerUser'.

🤖 Prompt for AI Agents
In src/app/api/polar/create-checkout/route.ts around lines 90 to 101, replace
the nonexistent polar.checkoutSessions.create call with polar.checkouts.create
and update the parameters: remove organizationId, replace productPriceId with
products: [productId], use returnUrl (not cancelUrl) for the redirect on cancel,
and move user-level fields into customerMetadata (e.g., customerEmail and
customerName) while keeping order/subscription fields like userId and userEmail
in metadata; also confirm whether user.displayName should be used instead of
user.name and wire that property into customerName if appropriate.


if (!checkout?.url) {
throw new Error("Polar checkout session did not include a redirect URL");
}

return NextResponse.json({
checkoutId: checkout.id,
url: checkout.url,
});
} catch (error) {
const details = getSanitizedErrorDetails(error);
const adminMessage =
error instanceof Error ? error.message : "Unknown Polar checkout error";

console.error("Error creating Polar checkout session:", error);

return NextResponse.json(
{
error: "Failed to create checkout session",
details: error instanceof Error ? error.message : "Unknown error"
},
{ status: 500 }
);

const isAuthError =
typeof details === "string" &&
(details.includes("Authentication failed") ||
details.includes("invalid or expired"));

const status = isAuthError ? 401 : 500;

return buildResponse(status, {
error: "Unable to start checkout",
details,
adminMessage,
});
}
}
Loading
Loading