Skip to content
Open
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
10 changes: 10 additions & 0 deletions apps/cli/src/helpers/addons/mcp-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,12 @@ function getAllMcpServers(config: ProjectConfig): McpServerDef[] {
name: "polar",
target: "https://mcp.polar.sh/mcp/polar-mcp",
},
{
key: "revenuecat",
label: "RevenueCat",
name: "revenuecat",
target: "https://mcp.revenuecat.ai/mcp",
},
];
}

Expand Down Expand Up @@ -261,6 +267,10 @@ export function getRecommendedMcpServers(
recommendedServerKeys.push("polar");
}

if (config.payments === "revenuecat") {
recommendedServerKeys.push("revenuecat");
}

return uniqueValues(recommendedServerKeys)
.map((serverKey) => serversByKey.get(serverKey))
.filter((server): server is McpServerDef => server !== undefined);
Expand Down
26 changes: 26 additions & 0 deletions apps/cli/src/helpers/core/post-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export async function displayPostInstallInstructions(
config.payments === "polar" && config.auth === "better-auth"
? getPolarInstructions(backend, packageManager)
: "";
const revenueCatInstructions =
config.payments === "revenuecat" ? getRevenueCatInstructions(backend, packageManager) : "";

const bunWebNativeWarning =
packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
Expand Down Expand Up @@ -237,6 +239,7 @@ export async function displayPostInstallInstructions(
if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
if (polarInstructions) output += `\n${polarInstructions.trim()}\n`;
if (revenueCatInstructions) output += `\n${revenueCatInstructions.trim()}\n`;

if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
Expand Down Expand Up @@ -606,6 +609,29 @@ function getPolarInstructions(backend: Backend, packageManager: string) {
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}`;
}

function getRevenueCatInstructions(backend: Backend, packageManager: string) {
const base =
`${pc.bold("RevenueCat Payments Setup:")}\n` +
`${pc.cyan("•")} Create a project, entitlement, and offering in ${pc.underline("https://app.revenuecat.com/")}\n` +
`${pc.cyan("•")} Set the public SDK keys in ${pc.white("apps/native/.env")}:\n` +
`${pc.white(" EXPO_PUBLIC_REVENUECAT_IOS_KEY=appl_your_ios_key")}\n` +
`${pc.white(" EXPO_PUBLIC_REVENUECAT_ANDROID_KEY=goog_your_android_key")}\n` +
`${pc.white(" EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID=pro")}`;

if (backend === "convex") {
const cmd = packageManager === "npm" ? "npx" : packageManager;
return (
`${base}\n` +
`${pc.cyan("•")} Set the webhook secret (min 32 chars) from ${pc.white("packages/backend")}:\n` +
`${pc.white(" cd packages/backend")}\n` +
`${pc.white(` ${cmd} convex env set REVENUECAT_WEBHOOK_AUTH your_webhook_secret`)}\n` +
`${pc.cyan("•")} Configure a RevenueCat webhook to ${pc.white("https://<your-convex-site-url>/webhooks/revenuecat")} using the same value as the Authorization header`
);
}

return base;
}

function getAlchemyDeployInstructions(
runCmd: string,
webDeploy: WebDeploy,
Expand Down
40 changes: 29 additions & 11 deletions apps/cli/src/prompts/payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function getPaymentsChoice(
payments?: Payments,
auth?: Auth,
backend?: Backend,
_frontends?: Frontend[],
frontends?: Frontend[],
previousValue?: Payments,
) {
if (payments !== undefined) return payments;
Expand All @@ -17,23 +17,41 @@ export async function getPaymentsChoice(
}

const isPolarCompatible = auth === "better-auth";
const hasNativeFrontend = (frontends ?? []).some(
(frontend) =>
frontend === "native-bare" ||
frontend === "native-uniwind" ||
frontend === "native-unistyles",
);
const isRevenueCatCompatible = hasNativeFrontend;

if (!isPolarCompatible) {
if (!isPolarCompatible && !isRevenueCatCompatible) {
return "none" as Payments;
}

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

if (isPolarCompatible) {
options.push({
value: "polar" as Payments,
label: "Polar",
hint: "Turn your software into a business. 6 lines of code.",
},
{
value: "none" as Payments,
label: "None",
hint: "No payments integration",
},
];
});
}

if (isRevenueCatCompatible) {
options.push({
value: "revenuecat" as Payments,
label: "RevenueCat",
hint: "In-app subscriptions and cross-platform monetization for mobile.",
});
}

options.push({
value: "none" as Payments,
label: "None",
hint: "No payments integration",
});

const response = await navigableSelect<Payments>({
message: "Select payments provider",
Expand Down
11 changes: 10 additions & 1 deletion apps/cli/src/utils/compatibility-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export function validatePaymentsCompatibility(
payments: Payments | undefined,
auth: Auth | undefined,
_backend: Backend | undefined,
_frontends: Frontend[] = [],
frontends: Frontend[] = [],
): ValidationResult {
if (!payments || payments === "none") return Result.ok(undefined);

Expand All @@ -449,6 +449,15 @@ export function validatePaymentsCompatibility(
}
}

if (payments === "revenuecat") {
const { native } = splitFrontends(frontends);
if (native.length === 0) {
return validationErr(
"RevenueCat payments requires a native frontend (native-bare, native-uniwind, or native-unistyles). Please select a native frontend or choose a different payments provider.",
);
}
}

return Result.ok(undefined);
}

Expand Down
22 changes: 21 additions & 1 deletion apps/cli/test/matrix/cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const MATRIX_NATIVE_FRONTENDS = [
] as const;
export const MATRIX_APIS = ["trpc", "orpc", "none"] as const;
export const MATRIX_AUTHS = ["better-auth", "clerk", "none"] as const;
export const MATRIX_PAYMENTS = ["polar", "none"] as const;
export const MATRIX_PAYMENTS = ["polar", "revenuecat", "none"] as const;
export const MATRIX_DB_SETUPS = [
"turso",
"neon",
Expand Down Expand Up @@ -321,6 +321,26 @@ export function createSmokeMatrixCases(): MatrixCase[] {
}
}

for (const convexAuth of ["none", "better-auth"] as const) {
pushUnique(configs, seen, {
payments: "revenuecat",
backend: "convex",
runtime: "none",
database: "none",
orm: "none",
api: "none",
auth: convexAuth,
frontend: ["native-bare"],
});
}

for (const nativeFrontend of MATRIX_NATIVE_FRONTENDS) {
pushUnique(configs, seen, {
payments: "revenuecat",
frontend: [nativeFrontend],
});
}

for (const dbSetup of MATRIX_DB_SETUPS) {
pushUnique(configs, seen, {
dbSetup,
Expand Down
20 changes: 20 additions & 0 deletions apps/cli/test/matrix/oracle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export type MatrixRule =
| "orm-mongodb-requires-mongoose-or-prisma"
| "orm-requires-database"
| "payments-polar-requires-better-auth"
| "payments-revenuecat-requires-native-frontend"
| "runtime-none-requires-terminal-backend"
| "server-deploy-requires-backend"
| "server-deploy-requires-workers-runtime"
Expand Down Expand Up @@ -63,6 +64,12 @@ const FULLSTACK_FRONTENDS: readonly Frontend[] = [
"astro",
] as const;

const NATIVE_FRONTENDS: readonly Frontend[] = [
"native-bare",
"native-uniwind",
"native-unistyles",
] as const;

const CONVEX_INCOMPATIBLE_FRONTENDS: readonly Frontend[] = ["solid", "astro"] as const;

const CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS: readonly Frontend[] = [
Expand Down Expand Up @@ -92,6 +99,10 @@ function hasWebFrontend(frontends: readonly Frontend[]) {
return hasFrontend(frontends, WEB_FRONTENDS);
}

function hasNativeFrontend(frontends: readonly Frontend[]) {
return hasFrontend(frontends, NATIVE_FRONTENDS);
}

function addRule(rules: Set<MatrixRule>, condition: boolean, rule: MatrixRule) {
if (condition) rules.add(rule);
}
Expand Down Expand Up @@ -306,6 +317,12 @@ function validatePayments(config: ProjectConfig, rules: Set<MatrixRule>) {
config.payments === "polar" && config.auth !== "better-auth",
"payments-polar-requires-better-auth",
);

addRule(
rules,
config.payments === "revenuecat" && !hasNativeFrontend(config.frontend),
"payments-revenuecat-requires-native-frontend",
);
}

function validateExamples(config: ProjectConfig, rules: Set<MatrixRule>) {
Expand Down Expand Up @@ -418,6 +435,9 @@ export function classifyMatrixError(message: string): MatrixRule | "unknown" {
if (message.includes("Polar payments requires Better Auth")) {
return "payments-polar-requires-better-auth";
}
if (message.includes("RevenueCat payments requires a native frontend")) {
return "payments-revenuecat-requires-native-frontend";
}
if (message.includes("'--runtime none' is only supported")) {
return "runtime-none-requires-terminal-backend";
}
Expand Down
18 changes: 18 additions & 0 deletions apps/web/src/app/(home)/new/_components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ export const hasClerkCompatibleFrontend = (webFrontend: string[], nativeFrontend
) ||
nativeFrontend.some((f) => ["native-bare", "native-uniwind", "native-unistyles"].includes(f));

export const hasNativeFrontend = (nativeFrontend: string[]) =>
nativeFrontend.some((f) => ["native-bare", "native-uniwind", "native-unistyles"].includes(f));

export const hasClerkCompatibleBackend = (backend: string) =>
clerkSupportedBackends.includes(backend as (typeof clerkSupportedBackends)[number]);

Expand Down Expand Up @@ -609,6 +612,15 @@ export const analyzeStackCompatibility = (stack: StackState): CompatibilityResul
}
}

if (nextStack.payments === "revenuecat" && !hasNativeFrontend(nextStack.nativeFrontend)) {
nextStack.payments = "none";
changed = true;
changes.push({
category: "payments",
message: "Payments set to 'None' (RevenueCat requires a native frontend)",
});
}

// ============================================
// ADDONS CONSTRAINTS
// ============================================
Expand Down Expand Up @@ -1081,6 +1093,12 @@ export const getDisabledReason = (
}
}

if (category === "payments" && optionId === "revenuecat") {
if (!hasNativeFrontend(currentStack.nativeFrontend)) {
return "RevenueCat requires a native frontend (Expo)";
}
}

// ============================================
// ADDONS CONSTRAINTS
// ============================================
Expand Down
8 changes: 8 additions & 0 deletions apps/web/src/lib/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,6 +476,14 @@ export const TECH_OPTIONS: Record<
color: "from-purple-400 to-purple-600",
default: false,
},
{
id: "revenuecat",
name: "RevenueCat",
description: "In-app subscriptions and cross-platform monetization for mobile.",
icon: `${ICON_BASE_URL}/revenuecat.svg`,
color: "from-red-400 to-red-600",
default: false,
},
Comment on lines +479 to +486

@coderabbitai coderabbitai Bot Jun 16, 2026

Copy link
Copy Markdown
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

🏁 Script executed:

#!/bin/bash
set -euo pipefail

URL="https://r2.better-t-stack.dev/icons/revenuecat.svg"
echo "Checking: $URL"
curl -sS -o /dev/null -w "HTTP %{http_code}\n" -I "$URL"

Repository: AmanVarshney01/create-better-t-stack

Length of output: 150


Upload RevenueCat icon asset to R2 before shipping.

The icon asset at https://r2.better-t-stack.dev/icons/revenuecat.svg is missing (HTTP 404). This will render a broken icon in the payments picker UI. Upload the asset to complete this feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged — revenuecat.svg needs uploading to the R2 icon bucket, which is a maintainer step (I can't push to that bucket). It's called out in the PR description, and the entry follows the existing polar.svg convention, so it resolves once the asset is added.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@danestves, understood — thanks for the context. Since the R2 upload is a maintainer step and already documented in the PR description, the implementation-side work is complete. The concern stays open only until the asset is actually uploaded.

Would you like me to open a GitHub issue to track the R2 asset upload (revenuecat.svgr2.better-t-stack.dev/icons/) so it doesn't get missed after this PR merges?

{
id: "none",
name: "No Payments",
Expand Down
41 changes: 40 additions & 1 deletion packages/template-generator/src/processors/env-vars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ function buildNativeVars(
frontend: string[],
backend: ProjectConfig["backend"],
auth: ProjectConfig["auth"],
payments: ProjectConfig["payments"],
): EnvVariable[] {
const hasAstro = frontend.includes("astro");
const hasSvelte = frontend.includes("svelte");
Expand Down Expand Up @@ -251,6 +252,26 @@ function buildNativeVars(
});
}

if (payments === "revenuecat") {
vars.push(
{
key: "EXPO_PUBLIC_REVENUECAT_IOS_KEY",
value: "",
condition: true,
},
{
key: "EXPO_PUBLIC_REVENUECAT_ANDROID_KEY",
value: "",
condition: true,
},
{
key: "EXPO_PUBLIC_REVENUECAT_ENTITLEMENT_ID",
value: "pro",
condition: true,
},
);
}

return vars;
}

Expand Down Expand Up @@ -363,6 +384,15 @@ function buildConvexBackendVars(
);
}

if (payments === "revenuecat") {
vars.push({
key: "REVENUECAT_WEBHOOK_AUTH",
value: "",
condition: true,
comment: "Shared secret for RevenueCat webhook authentication (min 32 characters)",
});
}

return vars;
}

Expand Down Expand Up @@ -419,6 +449,15 @@ ${needsConvexSiteUrl ? "# npx convex env set CONVEX_SITE_URL https://<YOUR_CONVE
# Create a Polar webhook at https://<your-convex-site-url>/polar/events
# Enable: product.created, product.updated, subscription.created, subscription.updated

`;
}

if (payments === "revenuecat") {
commentBlocks += `# Set RevenueCat environment variables
# npx convex env set REVENUECAT_WEBHOOK_AUTH your_webhook_secret_min_32_chars
# Create a RevenueCat webhook at https://<your-convex-site-url>/webhooks/revenuecat
# Set the webhook Authorization header to the same REVENUECAT_WEBHOOK_AUTH value

`;
}

Expand Down Expand Up @@ -606,7 +645,7 @@ export function processEnvVariables(vfs: VirtualFileSystem, config: ProjectConfi
const nativeDir = "apps/native";
if (vfs.directoryExists(nativeDir)) {
const envPath = `${nativeDir}/.env`;
const nativeVars = buildNativeVars(frontend, backend, auth);
const nativeVars = buildNativeVars(frontend, backend, auth, payments);
writeEnvFile(vfs, envPath, nativeVars);
}
}
Expand Down
Loading