Skip to content

Commit ecbd44f

Browse files
feat: add Polar support for Convex-backed Better Auth projects with CLI integration (#110)
* feat: add Polar support for Convex-backed Better Auth projects with CLI integration * fix: auth test for Convex Better Auth with Polar payments * update * fixes * fix: suppress TypeScript baseUrl deprecation and fix TinaCMS env variable access * fix: remove unrelated changes, fix dead imports and missing newlines - Revert unrelated elasticsearch feature (schema, deps, env vars, templates, tests, web constants/icons/links) — should be a separate PR - Revert ignoreDeprecations additions to 13 tsconfig files - Revert TinaCMS env var and README indentation changes - Revert unrelated svelte/next template tweaks - Remove dead useQuery/api imports from convex get-payment.ts.hbs - Add missing trailing newlines to 5 template files - Update snapshots to match reverted templates * fix: restore missing elasticsearch entries in web icons, links, and metadata * ci: retrigger CI * fix: restore elasticsearch support removed by mistake in Polar/Convex PR Reverts unrelated elasticsearch removals that were accidentally included in this PR. Elasticsearch was added in a separate merged PR and should not be removed here. * fix: restore elasticsearch env vars accidentally removed in Polar/Convex merge --------- Co-authored-by: Ibrahim Elkamali <126423069+Marve10s@users.noreply.github.com> Co-authored-by: Marve10s <igrimanigroman@gmail.com>
1 parent 908a568 commit ecbd44f

File tree

16 files changed

+483
-46
lines changed

16 files changed

+483
-46
lines changed

apps/cli/src/helpers/core/post-installation.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export async function displayPostInstallInstructions(
109109
config.auth === "clerk" ? getClerkInstructions(config.backend, config.frontend ?? []) : "";
110110
const polarInstructions =
111111
config.payments === "polar" && config.auth === "better-auth"
112-
? getPolarInstructions(backend)
112+
? getPolarInstructions(backend, packageManager)
113113
: "";
114114
const alchemyDeployInstructions = getAlchemyDeployInstructions(
115115
runCmd,
@@ -447,7 +447,21 @@ function getBetterAuthConvexInstructions(hasWeb: boolean, webPort: string, packa
447447
);
448448
}
449449

450-
function getPolarInstructions(backend: Backend) {
450+
function getPolarInstructions(backend: Backend, packageManager: string) {
451+
if (backend === "convex") {
452+
const cmd = packageManager === "npm" ? "npx" : packageManager;
453+
return (
454+
`${pc.bold("Polar Payments Setup:")}\n` +
455+
`${pc.cyan("•")} Create a Polar organization token, webhook secret, and product in ${pc.underline("https://sandbox.polar.sh/")}\n` +
456+
`${pc.cyan("•")} Set the Convex env vars from ${pc.white("packages/backend")}:\n` +
457+
`${pc.white(" cd packages/backend")}\n` +
458+
`${pc.white(` ${cmd} convex env set POLAR_ORGANIZATION_TOKEN=your_polar_token`)}\n` +
459+
`${pc.white(` ${cmd} convex env set POLAR_WEBHOOK_SECRET=your_polar_webhook_secret`)}\n` +
460+
`${pc.white(` ${cmd} convex env set POLAR_PRODUCT_ID_PRO=your_polar_product_id`)}\n` +
461+
`${pc.white(" Optional: set POLAR_SERVER=production when you go live")}\n` +
462+
`${pc.cyan("•")} Configure a Polar webhook to ${pc.white("https://<your-convex-site-url>/polar/events")}`
463+
);
464+
}
451465
const envPath = backend === "self" ? "apps/web/.env" : "apps/server/.env";
452466
return `${pc.bold("Polar Payments Setup:")}\n${pc.cyan("•")} Get access token & product ID from ${pc.underline("https://sandbox.polar.sh/")}\n${pc.cyan("•")} Set POLAR_ACCESS_TOKEN in ${envPath}`;
453467
}

apps/cli/src/prompts/payments.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ export async function getPaymentsChoice(
1717
return "none" as Payments;
1818
}
1919

20-
// Polar requires better-auth and non-convex backend
2120
const isPolarCompatible =
22-
auth === "better-auth" &&
23-
backend !== "convex" &&
24-
(frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
21+
auth === "better-auth" && (frontends?.length === 0 || splitFrontends(frontends).web.length > 0);
2522

2623
const options: Array<{ value: Payments; label: string; hint: string }> = [];
2724

apps/cli/test/auth.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,77 @@ describe("Authentication Configurations", () => {
187187
expectSuccess(result);
188188
});
189189

190+
it("should scaffold Convex Better Auth with Polar payments", async () => {
191+
const result = await runTRPCTest({
192+
projectName: "better-auth-convex-polar",
193+
auth: "better-auth",
194+
payments: "polar",
195+
backend: "convex",
196+
runtime: "none",
197+
database: "none",
198+
orm: "none",
199+
api: "none",
200+
frontend: ["tanstack-router"],
201+
addons: ["turborepo"],
202+
examples: ["none"],
203+
dbSetup: "none",
204+
webDeploy: "none",
205+
serverDeploy: "none",
206+
install: false,
207+
});
208+
209+
expectSuccess(result);
210+
if (!result.projectDir) {
211+
throw new Error("Expected projectDir to be defined");
212+
}
213+
214+
const convexConfigFile = await readFile(
215+
join(result.projectDir, "packages/backend/convex/convex.config.ts"),
216+
"utf8",
217+
);
218+
const httpFile = await readFile(
219+
join(result.projectDir, "packages/backend/convex/http.ts"),
220+
"utf8",
221+
);
222+
const polarFile = await readFile(
223+
join(result.projectDir, "packages/backend/convex/polar.ts"),
224+
"utf8",
225+
);
226+
const dashboardFile = await readFile(
227+
join(result.projectDir, "apps/web/src/routes/dashboard.tsx"),
228+
"utf8",
229+
);
230+
const backendPackageFile = await readFile(
231+
join(result.projectDir, "packages/backend/package.json"),
232+
"utf8",
233+
);
234+
const webPackageFile = await readFile(
235+
join(result.projectDir, "apps/web/package.json"),
236+
"utf8",
237+
);
238+
const convexEnvFile = await readFile(
239+
join(result.projectDir, "packages/backend/.env.local"),
240+
"utf8",
241+
);
242+
243+
expect(convexConfigFile).toContain('import polar from "@convex-dev/polar/convex.config";');
244+
expect(convexConfigFile).toContain("app.use(polar);");
245+
expect(httpFile).toContain('import { polar } from "./polar";');
246+
expect(httpFile).toContain("polar.registerRoutes(http as any);");
247+
expect(polarFile).toContain('import { Polar } from "@convex-dev/polar";');
248+
expect(polarFile).toContain("POLAR_PRODUCT_ID_PRO");
249+
expect(dashboardFile).toContain('from "@convex-dev/polar/react";');
250+
expect(dashboardFile).toContain("api.polar.getConfiguredProducts");
251+
expect(dashboardFile).toContain("api.polar.getCurrentSubscription");
252+
expect(backendPackageFile).toContain('"@convex-dev/polar"');
253+
expect(backendPackageFile).toContain('"@polar-sh/sdk"');
254+
expect(webPackageFile).toContain('"@convex-dev/polar"');
255+
expect(webPackageFile).toContain('"@polar-sh/checkout"');
256+
expect(convexEnvFile).toContain("# npx convex env set POLAR_ORGANIZATION_TOKEN");
257+
expect(convexEnvFile).toContain("# npx convex env set POLAR_PRODUCT_ID_PRO");
258+
expect(convexEnvFile).toContain("POLAR_SERVER=sandbox");
259+
});
260+
190261
const compatibleFrontends = [
191262
"react-vite",
192263
"tanstack-router",

packages/template-generator/src/processors/env-vars.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ function buildNativeVars(
460460
function buildConvexBackendVars(
461461
frontend: string[],
462462
auth: ProjectConfig["auth"],
463+
payments: ProjectConfig["payments"],
463464
examples: ProjectConfig["examples"],
464465
): EnvVariable[] {
465466
const hasNextJs = frontend.includes("next");
@@ -516,12 +517,42 @@ function buildConvexBackendVars(
516517
}
517518
}
518519

520+
if (payments === "polar") {
521+
vars.push(
522+
{
523+
key: "POLAR_ORGANIZATION_TOKEN",
524+
value: "",
525+
condition: true,
526+
comment: "Polar organization token",
527+
},
528+
{
529+
key: "POLAR_WEBHOOK_SECRET",
530+
value: "",
531+
condition: true,
532+
comment: "Polar webhook secret",
533+
},
534+
{
535+
key: "POLAR_PRODUCT_ID_PRO",
536+
value: "",
537+
condition: true,
538+
comment: "Polar product ID for the default Pro plan",
539+
},
540+
{
541+
key: "POLAR_SERVER",
542+
value: "sandbox",
543+
condition: true,
544+
comment: "Polar environment: sandbox or production",
545+
},
546+
);
547+
}
548+
519549
return vars;
520550
}
521551

522552
function buildConvexCommentBlocks(
523553
frontend: string[],
524554
auth: ProjectConfig["auth"],
555+
payments: ProjectConfig["payments"],
525556
examples: ProjectConfig["examples"],
526557
): string {
527558
const hasWeb =
@@ -549,6 +580,18 @@ function buildConvexCommentBlocks(
549580
${hasWeb ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}`;
550581
}
551582

583+
if (payments === "polar") {
584+
commentBlocks += `# Set Polar environment variables
585+
# npx convex env set POLAR_ORGANIZATION_TOKEN=your_polar_token
586+
# npx convex env set POLAR_WEBHOOK_SECRET=your_polar_webhook_secret
587+
# npx convex env set POLAR_PRODUCT_ID_PRO=your_polar_product_id
588+
# Optional: npx convex env set POLAR_SERVER=sandbox
589+
# Create a Polar webhook at https://<your-convex-site-url>/polar/events
590+
# Enable: product.created, product.updated, subscription.created, subscription.updated
591+
592+
`;
593+
}
594+
552595
return commentBlocks;
553596
}
554597

@@ -1440,7 +1483,7 @@ export function processEnvVariables(vfs: VirtualFileSystem, config: ProjectConfi
14401483
const envLocalPath = `${convexBackendDir}/.env.local`;
14411484

14421485
// Write comment blocks first
1443-
const commentBlocks = buildConvexCommentBlocks(frontend, auth, examples);
1486+
const commentBlocks = buildConvexCommentBlocks(frontend, auth, payments, examples);
14441487
if (commentBlocks) {
14451488
let currentContent = "";
14461489
if (vfs.exists(envLocalPath)) {
@@ -1450,7 +1493,7 @@ export function processEnvVariables(vfs: VirtualFileSystem, config: ProjectConfi
14501493
}
14511494

14521495
// Then add variables
1453-
const convexBackendVars = buildConvexBackendVars(frontend, auth, examples);
1496+
const convexBackendVars = buildConvexBackendVars(frontend, auth, payments, examples);
14541497
if (convexBackendVars.length > 0) {
14551498
let existingContent = "";
14561499
if (vfs.exists(envLocalPath)) {

packages/template-generator/src/processors/payments-deps.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,37 @@ export function processPaymentsDeps(vfs: VirtualFileSystem, config: ProjectConfi
2525
const { payments, frontend, backend } = config;
2626
if (!payments || payments === "none") return;
2727

28+
const backendPath = "packages/backend/package.json";
2829
const authPath = "packages/auth/package.json";
2930
const webPath = getWebPackagePath(frontend, backend);
3031
const serverPath = getServerPackagePath(frontend, backend);
3132

3233
if (payments === "polar") {
34+
if (backend === "convex") {
35+
if (vfs.exists(backendPath)) {
36+
addPackageDependency({
37+
vfs,
38+
packagePath: backendPath,
39+
dependencies: ["@convex-dev/polar", "@polar-sh/sdk"],
40+
});
41+
}
42+
43+
if (vfs.exists(webPath)) {
44+
const hasReactWebFrontend = frontend.some((f) =>
45+
["react-router", "tanstack-router", "tanstack-start", "next"].includes(f),
46+
);
47+
if (hasReactWebFrontend) {
48+
addPackageDependency({
49+
vfs,
50+
packagePath: webPath,
51+
dependencies: ["@convex-dev/polar", "@polar-sh/checkout"],
52+
});
53+
}
54+
}
55+
56+
return;
57+
}
58+
3359
if (vfs.exists(authPath)) {
3460
addPackageDependency({
3561
vfs,

packages/template-generator/src/template-handlers/payments.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export async function processPaymentsTemplates(
1010
config: ProjectConfig,
1111
): Promise<void> {
1212
if (!config.payments || config.payments === "none") return;
13-
if (config.backend === "convex") return;
1413

1514
const hasReactWeb = config.frontend.some((f) =>
1615
["tanstack-router", "react-router", "react-vite", "tanstack-start", "next"].includes(f),
@@ -19,7 +18,15 @@ export async function processPaymentsTemplates(
1918
const hasSvelteWeb = config.frontend.includes("svelte");
2019
const hasSolidWeb = config.frontend.includes("solid");
2120

22-
if (config.backend !== "none") {
21+
if (config.backend === "convex") {
22+
processTemplatesFromPrefix(
23+
vfs,
24+
templates,
25+
`payments/${config.payments}/convex/backend`,
26+
"packages/backend",
27+
config,
28+
);
29+
} else if (config.backend !== "none") {
2330
processTemplatesFromPrefix(
2431
vfs,
2532
templates,
@@ -43,6 +50,15 @@ export async function processPaymentsTemplates(
4350
"apps/web",
4451
config,
4552
);
53+
if (config.backend === "convex") {
54+
processTemplatesFromPrefix(
55+
vfs,
56+
templates,
57+
`payments/${config.payments}/convex/web/react/${reactFramework}`,
58+
"apps/web",
59+
config,
60+
);
61+
}
4662
}
4763
} else if (hasNuxtWeb) {
4864
processTemplatesFromPrefix(

packages/template-generator/src/utils/add-deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ export const dependencyVersionMap = {
228228
convex: "^1.34.1",
229229
"@convex-dev/react-query": "^0.1.0",
230230
"@convex-dev/agent": "^0.6.1",
231+
"@convex-dev/polar": "^0.9.0",
231232
"convex-svelte": "^0.0.12",
232233
"convex-nuxt": "0.1.5",
233234
"convex-vue": "^0.1.5",
@@ -306,6 +307,7 @@ export const dependencyVersionMap = {
306307
srvx: "^0.11.13",
307308

308309
"@polar-sh/better-auth": "^1.8.3",
310+
"@polar-sh/checkout": "^0.2.0",
309311
"@polar-sh/sdk": "^0.46.7",
310312

311313
// Email
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { httpRouter } from "convex/server";
22
import { authComponent, createAuth } from "./auth";
33

4+
{{#if (eq payments "polar")}}
5+
import { polar } from "./polar";
6+
{{/if}}
7+
48
const http = httpRouter();
59

610
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
711
authComponent.registerRoutes(http, createAuth, { cors: true });
812
{{else}}
913
authComponent.registerRoutes(http, createAuth);
1014
{{/if}}
15+
{{#if (eq payments "polar")}}
16+
17+
polar.registerRoutes(http as any);
18+
{{/if}}
1119

1220
export default http;

0 commit comments

Comments
 (0)