Skip to content

Commit f089944

Browse files
feat(cli): add better-auth + convex + native app support (#634)
1 parent e4a3708 commit f089944

File tree

29 files changed

+998
-194
lines changed

29 files changed

+998
-194
lines changed

apps/cli/src/helpers/addons/ruler-setup.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,16 @@ export async function setupRuler(config: ProjectConfig) {
4646
crush: { label: "Crush" },
4747
zed: { label: "Zed" },
4848
qwen: { label: "Qwen" },
49-
amazonqcli: {label: "Amazon Q CLI"},
50-
augmentcode: {label: "AugmentCode"},
51-
firebender: {label: "Firebender"},
52-
goose: {label: "Goose"},
53-
jules: {label: "Jules"},
54-
kiro: {label: "Kiro"},
55-
openhands: {label: "Open Hands"},
56-
roo: {label: "RooCode"},
57-
trae: {label: "Trae AI"},
58-
warp: {label: "Warp"},
49+
amazonqcli: { label: "Amazon Q CLI" },
50+
augmentcode: { label: "AugmentCode" },
51+
firebender: { label: "Firebender" },
52+
goose: { label: "Goose" },
53+
jules: { label: "Jules" },
54+
kiro: { label: "Kiro" },
55+
openhands: { label: "Open Hands" },
56+
roo: { label: "RooCode" },
57+
trae: { label: "Trae AI" },
58+
warp: { label: "Warp" },
5959
} as const;
6060

6161
const selectedEditors = await autocompleteMultiselect({

apps/cli/src/helpers/core/auth-setup.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,23 @@ export async function setupAuth(config: ProjectConfig) {
5151
const convexBackendDir = path.join(projectDir, "packages/backend");
5252
const convexBackendDirExists = await fs.pathExists(convexBackendDir);
5353

54+
const hasNativeForBA =
55+
frontend.includes("native-nativewind") ||
56+
frontend.includes("native-unistyles");
57+
5458
if (convexBackendDirExists) {
5559
await addPackageDependency({
5660
dependencies: ["better-auth", "@convex-dev/better-auth"],
5761
customDependencies: { "better-auth": "1.3.27" },
5862
projectDir: convexBackendDir,
5963
});
64+
if (hasNativeForBA) {
65+
await addPackageDependency({
66+
dependencies: ["@better-auth/expo"],
67+
customDependencies: { "@better-auth/expo": "1.3.27" },
68+
projectDir: convexBackendDir,
69+
});
70+
}
6071
}
6172

6273
if (clientDirExists) {
@@ -86,6 +97,23 @@ export async function setupAuth(config: ProjectConfig) {
8697
});
8798
}
8899
}
100+
101+
const hasNativeWind = frontend.includes("native-nativewind");
102+
const hasUnistyles = frontend.includes("native-unistyles");
103+
if (nativeDirExists && (hasNativeWind || hasUnistyles)) {
104+
await addPackageDependency({
105+
dependencies: [
106+
"better-auth",
107+
"@better-auth/expo",
108+
"@convex-dev/better-auth",
109+
],
110+
customDependencies: {
111+
"better-auth": "1.3.27",
112+
"@better-auth/expo": "1.3.27",
113+
},
114+
projectDir: nativeDir,
115+
});
116+
}
89117
}
90118

91119
const hasNativeWind = frontend.includes("native-nativewind");

apps/cli/src/helpers/core/env-setup.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,14 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
258258
condition: true,
259259
});
260260
}
261+
262+
if (backend === "convex" && auth === "better-auth") {
263+
nativeVars.push({
264+
key: "EXPO_PUBLIC_CONVEX_SITE_URL",
265+
value: "https://<YOUR_CONVEX_URL>",
266+
condition: true,
267+
});
268+
}
261269
await addEnvVariablesToFile(path.join(nativeDir, ".env"), nativeVars);
262270
}
263271
}
@@ -268,6 +276,11 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
268276
if (await fs.pathExists(convexBackendDir)) {
269277
const envLocalPath = path.join(convexBackendDir, ".env.local");
270278

279+
const hasNative =
280+
frontend.includes("native-nativewind") ||
281+
frontend.includes("native-unistyles");
282+
const hasWeb = hasWebFrontend;
283+
271284
if (
272285
!(await fs.pathExists(envLocalPath)) ||
273286
!(await fs.readFile(envLocalPath, "utf8")).includes(
@@ -276,27 +289,40 @@ export async function setupEnvironmentVariables(config: ProjectConfig) {
276289
) {
277290
const convexCommands = `# Set Convex environment variables
278291
# npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
279-
# npx convex env set SITE_URL http://localhost:3001
280-
292+
${hasWeb ? "# npx convex env set SITE_URL http://localhost:3001\n" : ""}
281293
`;
282294
await fs.appendFile(envLocalPath, convexCommands);
283295
}
284296

285-
const convexBackendVars: EnvVariable[] = [
286-
{
287-
key: hasNextJs
288-
? "NEXT_PUBLIC_CONVEX_SITE_URL"
289-
: "VITE_CONVEX_SITE_URL",
297+
const convexBackendVars: EnvVariable[] = [];
298+
299+
if (hasNative) {
300+
convexBackendVars.push({
301+
key: "EXPO_PUBLIC_CONVEX_SITE_URL",
290302
value: "",
291303
condition: true,
292304
comment: "Same as CONVEX_URL but ends in .site",
293-
},
294-
{
295-
key: "SITE_URL",
296-
value: "http://localhost:3001",
297-
condition: true,
298-
},
299-
];
305+
});
306+
}
307+
308+
if (hasWeb) {
309+
convexBackendVars.push(
310+
{
311+
key: hasNextJs
312+
? "NEXT_PUBLIC_CONVEX_SITE_URL"
313+
: "VITE_CONVEX_SITE_URL",
314+
value: "",
315+
condition: true,
316+
comment: "Same as CONVEX_URL but ends in .site",
317+
},
318+
{
319+
key: "SITE_URL",
320+
value: "http://localhost:3001",
321+
condition: true,
322+
},
323+
);
324+
}
325+
300326
await addEnvVariablesToFile(envLocalPath, convexBackendVars);
301327
}
302328
}

apps/cli/src/helpers/core/template-manager.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ export async function setupFrontendTemplates(
207207

208208
const nativeBaseCommonDir = path.join(
209209
PKG_ROOT,
210-
"templates/frontend/native/native-base",
210+
"templates/frontend/native/base",
211211
);
212212
if (await fs.pathExists(nativeBaseCommonDir)) {
213213
await processAndCopyFiles(
@@ -513,6 +513,39 @@ export async function setupAuthTemplate(
513513
}
514514
}
515515
}
516+
517+
if (nativeAppDirExists) {
518+
const convexBetterAuthNativeBaseSrc = path.join(
519+
PKG_ROOT,
520+
"templates/auth/better-auth/convex/native/base",
521+
);
522+
if (await fs.pathExists(convexBetterAuthNativeBaseSrc)) {
523+
await processAndCopyFiles(
524+
"**/*",
525+
convexBetterAuthNativeBaseSrc,
526+
nativeAppDir,
527+
context,
528+
);
529+
}
530+
531+
let nativeFrameworkPath = "";
532+
if (hasNativeWind) nativeFrameworkPath = "nativewind";
533+
else if (hasUnistyles) nativeFrameworkPath = "unistyles";
534+
if (nativeFrameworkPath) {
535+
const convexBetterAuthNativeFrameworkSrc = path.join(
536+
PKG_ROOT,
537+
`templates/auth/better-auth/convex/native/${nativeFrameworkPath}`,
538+
);
539+
if (await fs.pathExists(convexBetterAuthNativeFrameworkSrc)) {
540+
await processAndCopyFiles(
541+
"**/*",
542+
convexBetterAuthNativeFrameworkSrc,
543+
nativeAppDir,
544+
context,
545+
);
546+
}
547+
}
548+
}
516549
return;
517550
}
518551

@@ -645,7 +678,7 @@ export async function setupAuthTemplate(
645678
if (hasNative && nativeAppDirExists) {
646679
const authNativeBaseSrc = path.join(
647680
PKG_ROOT,
648-
`templates/auth/${authProvider}/native/native-base`,
681+
`templates/auth/${authProvider}/native/base`,
649682
);
650683
if (await fs.pathExists(authNativeBaseSrc)) {
651684
await processAndCopyFiles(

apps/cli/src/prompts/auth.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ export async function getAuthChoice(
1212
if (auth !== undefined) return auth;
1313
if (backend === "convex") {
1414
const supportedBetterAuthFrontends = frontend?.some((f) =>
15-
["tanstack-router", "tanstack-start", "next"].includes(f),
15+
[
16+
"tanstack-router",
17+
"tanstack-start",
18+
"next",
19+
"native-nativewind",
20+
"native-unistyles",
21+
].includes(f),
1622
);
1723

1824
const hasClerkCompatibleFrontends = frontend?.some((f) =>

apps/cli/src/utils/config-validation.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,20 @@ export function validateConvexConstraints(
235235
}
236236

237237
if (has("auth") && config.auth === "better-auth") {
238-
const supportedFrontends = ["tanstack-router", "tanstack-start", "next"];
238+
const supportedFrontends = [
239+
"tanstack-router",
240+
"tanstack-start",
241+
"next",
242+
"native-nativewind",
243+
"native-unistyles",
244+
];
239245
const hasSupportedFrontend = config.frontend?.some((f) =>
240246
supportedFrontends.includes(f),
241247
);
242248

243249
if (!hasSupportedFrontend) {
244250
exitWithError(
245-
"Better-Auth with Convex backend is only supported with TanStack Router, TanStack Start, or Next.js frontends. Please use '--auth clerk' or '--auth none'.",
251+
"Better-Auth with Convex backend requires a supported frontend (TanStack Router, TanStack Start, Next.js, or Native).",
246252
);
247253
}
248254
}
Lines changed: 55 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,70 @@
11
import { createClient, type GenericCtx } from "@convex-dev/better-auth";
2-
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
2+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
33
import { convex } from "@convex-dev/better-auth/plugins";
4+
import { expo } from "@better-auth/expo";
45
{{else}}
5-
import { convex, crossDomain } from "@convex-dev/better-auth/plugins";
6+
import { convex } from "@convex-dev/better-auth/plugins";
7+
{{/if}}
8+
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
9+
import { crossDomain } from "@convex-dev/better-auth/plugins";
610
{{/if}}
711
import { components } from "./_generated/api";
812
import { DataModel } from "./_generated/dataModel";
913
import { query } from "./_generated/server";
1014
import { betterAuth } from "better-auth";
15+
import { v } from "convex/values";
1116

17+
{{#if (or (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
1218
const siteUrl = process.env.SITE_URL!;
19+
{{/if}}
20+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
21+
const nativeAppUrl = process.env.NATIVE_APP_URL || "mybettertapp://";
22+
{{/if}}
1323

1424
export const authComponent = createClient<DataModel>(components.betterAuth);
1525

16-
export const createAuth = (
17-
ctx: GenericCtx<DataModel>,
18-
{ optionsOnly } = { optionsOnly: false },
19-
) => {
20-
return betterAuth({
21-
logger: {
22-
disabled: optionsOnly,
23-
},
24-
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
25-
baseURL: siteUrl,
26-
trustedOrigins: [siteUrl],
27-
{{else}}
28-
trustedOrigins: [siteUrl],
29-
{{/if}}
30-
database: authComponent.adapter(ctx),
31-
emailAndPassword: {
32-
enabled: true,
33-
requireEmailVerification: false,
34-
},
35-
plugins: [
36-
{{#unless (or (includes frontend "tanstack-start") (includes frontend "next"))}}
37-
crossDomain({ siteUrl }),
38-
{{/unless}}
39-
convex(),
40-
],
41-
});
42-
};
26+
function createAuth(
27+
ctx: GenericCtx<DataModel>,
28+
{ optionsOnly }: { optionsOnly?: boolean } = { optionsOnly: false }
29+
) {
30+
return betterAuth({
31+
logger: {
32+
disabled: optionsOnly,
33+
},
34+
{{#if (and (or (includes frontend "native-nativewind") (includes frontend "native-unistyles")) (or (includes frontend "tanstack-start") (includes frontend "next")))}}
35+
baseURL: siteUrl,
36+
trustedOrigins: [siteUrl, nativeAppUrl],
37+
{{else if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
38+
trustedOrigins: [nativeAppUrl],
39+
{{else if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
40+
baseURL: siteUrl,
41+
trustedOrigins: [siteUrl],
42+
{{else}}
43+
trustedOrigins: [siteUrl],
44+
{{/if}}
45+
database: authComponent.adapter(ctx),
46+
emailAndPassword: {
47+
enabled: true,
48+
requireEmailVerification: false,
49+
},
50+
plugins: [
51+
{{#if (or (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
52+
expo(),
53+
{{/if}}
54+
{{#if (or (includes frontend "tanstack-router") (includes frontend "react-router") (includes frontend "nuxt") (includes frontend "svelte") (includes frontend "solid"))}}
55+
crossDomain({ siteUrl }),
56+
{{/if}}
57+
convex(),
58+
],
59+
});
60+
}
61+
62+
export { createAuth };
4363

4464
export const getCurrentUser = query({
45-
args: {},
46-
handler: async (ctx) => {
47-
return authComponent.getAuthUser(ctx);
48-
},
49-
});
65+
args: {},
66+
returns: v.any(),
67+
handler: async function (ctx, args) {
68+
return authComponent.getAuthUser(ctx);
69+
},
70+
});

apps/cli/templates/auth/better-auth/convex/backend/convex/http.ts.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { authComponent, createAuth } from "./auth";
33

44
const http = httpRouter();
55

6-
{{#if (or (includes frontend "tanstack-start") (includes frontend "next"))}}
6+
{{#if (or (includes frontend "tanstack-start") (includes frontend "next") (includes frontend "native-nativewind") (includes frontend "native-unistyles"))}}
77
authComponent.registerRoutes(http, createAuth);
88
{{else}}
99
authComponent.registerRoutes(http, createAuth, { cors: true });
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createAuthClient } from "better-auth/react";
2+
import { anonymousClient } from "better-auth/client/plugins";
3+
import { convexClient } from "@convex-dev/better-auth/client/plugins";
4+
import { expoClient } from "@better-auth/expo/client";
5+
import Constants from "expo-constants";
6+
import * as SecureStore from "expo-secure-store";
7+
8+
export const authClient = createAuthClient({
9+
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL,
10+
plugins: [
11+
anonymousClient(),
12+
expoClient({
13+
scheme: Constants.expoConfig?.scheme as string,
14+
storagePrefix: Constants.expoConfig?.scheme as string,
15+
storage: SecureStore,
16+
}),
17+
convexClient(),
18+
],
19+
});

0 commit comments

Comments
 (0)