diff --git a/.changeset/sour-brooms-fail.md b/.changeset/sour-brooms-fail.md
new file mode 100644
index 00000000..86fc04c9
--- /dev/null
+++ b/.changeset/sour-brooms-fail.md
@@ -0,0 +1,5 @@
+---
+'create-expo-stack': minor
+---
+
+Added Convex + Better Auth in the authentication choices
diff --git a/cli/src/commands/create-expo-stack.ts b/cli/src/commands/create-expo-stack.ts
index 5e343827..0150f2a7 100644
--- a/cli/src/commands/create-expo-stack.ts
+++ b/cli/src/commands/create-expo-stack.ts
@@ -307,6 +307,14 @@ const command: GluegunCommand = {
});
}
+ if (options.convex) {
+ // Add convex package
+ cliResults.packages.push({
+ name: 'convex',
+ type: 'authentication'
+ });
+ }
+
// State Management packages
if (options.zustand) {
// Add zustand package
diff --git a/cli/src/templates/base/.gitignore.ejs b/cli/src/templates/base/.gitignore.ejs
index 5040396e..f369dd07 100644
--- a/cli/src/templates/base/.gitignore.ejs
+++ b/cli/src/templates/base/.gitignore.ejs
@@ -9,12 +9,21 @@ npm-debug.*
*.mobileprovision
*.orig.*
web-build/
-<% if (props.navigationPackage?.name === "expo-router") { %># expo router
-expo-env.d.ts<% } %>
-<% if (props.stylingPackage?.name === "tamagui") { %># tamagui
-.tamagui/<% } %>
-<% if ((props.authenticationPackage?.name === "supabase") || (props.authenticationPackage?.name === "firebase" || (props.analyticsPackage?.name === 'vexo-analytics'))) { %># firebase/supabase/vexo
-.env<% } %>
+<% if (props.navigationPackage?.name === "expo-router") { %>
+# expo router
+expo-env.d.ts
+<% } %>
+<% if (props.stylingPackage?.name === "tamagui") { %>
+# tamagui
+.tamagui/
+<% } %>
+<% if ((props.authenticationPackage?.name === "supabase") || (props.authenticationPackage?.name === "firebase" || (props.analyticsPackage?.name === 'vexo-analytics'))) { %>
+# firebase/supabase/vexo
+.env
+<% } %>
+<% if (props.authenticationPackage?.name === "convex") { %>
+.env.local
+<% } %>
ios
android
diff --git a/cli/src/templates/base/App.tsx.ejs b/cli/src/templates/base/App.tsx.ejs
index e3dc807e..9fbc0dcc 100644
--- a/cli/src/templates/base/App.tsx.ejs
+++ b/cli/src/templates/base/App.tsx.ejs
@@ -21,17 +21,39 @@ import { StatusBar } from 'expo-status-bar';
vexo(process.env.EXPO_PUBLIC_VEXO_API_KEY);
<% } %>
+<% if (props.authenticationPackage?.name === "convex") { %>
+ import { ConvexReactClient } from "convex/react";
+ import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
+ import { authClient } from "@/lib/auth-client";
+
+ const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, {
+ // Optionally pause queries until the user is authenticated
+ expectAuth: true,
+ unsavedChangesWarning: false,
+ });
+<% } %>
export default function App() {
return (
- <>
-
- <% if (props.internalizationPackage?.name === "i18next") { %>
-
- <% } %>
-
-
+ <% if (props.authenticationPackage?.name === "convex") { %>
+
+
+ <% if (props.internalizationPackage?.name === "i18next") { %>
+
+ <% } %>
+
+
+
+ <% } else {%>
+ <>
+
+ <% if (props.internalizationPackage?.name === "i18next") { %>
+
+ <% } %>
+
+
>
+ <%}%>
);
}
diff --git a/cli/src/templates/base/app.json.ejs b/cli/src/templates/base/app.json.ejs
index 079ca7bb..f57e058f 100644
--- a/cli/src/templates/base/app.json.ejs
+++ b/cli/src/templates/base/app.json.ejs
@@ -6,8 +6,8 @@
<% if (props.stylingPackage?.name === "unistyles") { %>
"newArchEnabled": true,
<% } %>
+ "scheme": "<%= props.projectName %>",
<% if (props.navigationPackage?.name === 'expo-router') { %>
- "scheme": "<%= props.projectName %>",
"platforms": ["ios", "android"],
"web": {
"bundler": "metro",
diff --git a/cli/src/templates/base/babel.config.js.ejs b/cli/src/templates/base/babel.config.js.ejs
index cc589bff..bb0655cb 100644
--- a/cli/src/templates/base/babel.config.js.ejs
+++ b/cli/src/templates/base/babel.config.js.ejs
@@ -12,6 +12,19 @@ module.exports = function(api) {
]);
<% } %>
+ <% if (props.authenticationPackage?.name === "convex") { %>
+ plugins.push([
+ "module-resolver",
+ {
+ alias: {
+ "better-auth/react": "./node_modules/better-auth/dist/client/react/index.cjs",
+ "better-auth/client/plugins": "./node_modules/better-auth/dist/client/plugins/index.cjs",
+ "@better-auth/expo/client": "./node_modules/@better-auth/expo/dist/client.cjs",
+ },
+ },
+ ]);
+ <% } %>
+
plugins.push('react-native-worklets/plugin');
return {
diff --git a/cli/src/templates/base/eslint.config.js.ejs b/cli/src/templates/base/eslint.config.js.ejs
index 514d7086..5e48bb76 100644
--- a/cli/src/templates/base/eslint.config.js.ejs
+++ b/cli/src/templates/base/eslint.config.js.ejs
@@ -5,7 +5,12 @@ const expoConfig = require('eslint-config-expo/flat');
module.exports = defineConfig([
expoConfig,
{
- ignores: ['dist/*'],
+ ignores: [
+ 'dist/*',
+ <% if (props.authenticationPackage?.name === "convex") { %>
+ 'convex/*'
+ <% } %>
+ ],
},
{
rules: {
diff --git a/cli/src/templates/base/package.json.ejs b/cli/src/templates/base/package.json.ejs
index ddeafbf0..79f3389f 100644
--- a/cli/src/templates/base/package.json.ejs
+++ b/cli/src/templates/base/package.json.ejs
@@ -116,6 +116,14 @@
"firebase": "^10.5.2",
<% } %>
+ <% if (props.authenticationPackage?.name === "convex") { %>
+ "@convex-dev/better-auth": "^0.9.7",
+ "@better-auth/expo": "1.3.34",
+ "better-auth": "1.3.34",
+ "convex": "^1.29.3",
+ "expo-secure-store": "~15.0.7",
+ <% } %>
+
<% if (props.internalizationPackage?.name === "i18next") { %>
"i18next": "^23.7.20",
"react-i18next": "^14.0.1",
@@ -138,6 +146,9 @@
"devDependencies": {
"@babel/core": "^7.20.0",
"@types/react": "~19.1.10",
+ <% if (props.authenticationPackage?.name === "convex") { %>
+ "babel-plugin-module-resolver": "^5.0.2",
+ <% } %>
"eslint": "^9.25.1",
"eslint-config-expo": "~10.0.0",
"eslint-config-prettier": "^10.1.2",
diff --git a/cli/src/templates/base/tsconfig.json.ejs b/cli/src/templates/base/tsconfig.json.ejs
index 92548be3..10a39f18 100644
--- a/cli/src/templates/base/tsconfig.json.ejs
+++ b/cli/src/templates/base/tsconfig.json.ejs
@@ -11,7 +11,7 @@
<% if (props.navigationPackage?.name === "expo-router" && props.flags.importAlias === true) { %>
"@/*": ["*"]
<% } else if (props.flags.importAlias === true) { %>
- "@/*": ["src/*"]
+ "@/*": ["src/*", "*"]
<% } else { %>
"<%= props.flags.importAlias %>": ["src/*"]
<% } %>
diff --git a/cli/src/templates/packages/convex/.env.local.ejs b/cli/src/templates/packages/convex/.env.local.ejs
new file mode 100644
index 00000000..2c38b745
--- /dev/null
+++ b/cli/src/templates/packages/convex/.env.local.ejs
@@ -0,0 +1,4 @@
+# Start by running: npx convex dev, to login and create your Convex backend
+# Then set EXPO_PUBLIC_CONVEX_URL_SITE as the same as EXPO_PUBLIC_CONVEX_URL but ends in .site, it can also be found as the HTTP Actions URL in the URL & Deploy Key tab of your Convex dashboard
+# Finaly run this command once to set your better auth secret in your convex dashboard: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
+EXPO_PUBLIC_CONVEX_SITE_URL=
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/convex/auth.config.ts.ejs b/cli/src/templates/packages/convex/convex/auth.config.ts.ejs
new file mode 100644
index 00000000..18740698
--- /dev/null
+++ b/cli/src/templates/packages/convex/convex/auth.config.ts.ejs
@@ -0,0 +1,8 @@
+export default {
+ providers: [
+ {
+ domain: process.env.CONVEX_SITE_URL,
+ applicationID: "convex",
+ },
+ ],
+};
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/convex/auth.ts.ejs b/cli/src/templates/packages/convex/convex/auth.ts.ejs
new file mode 100644
index 00000000..56758dba
--- /dev/null
+++ b/cli/src/templates/packages/convex/convex/auth.ts.ejs
@@ -0,0 +1,47 @@
+import { createClient, type GenericCtx } from "@convex-dev/better-auth";
+import { convex } from "@convex-dev/better-auth/plugins";
+import { betterAuth } from "better-auth";
+import { expo } from '@better-auth/expo';
+import { components } from "./_generated/api";
+import { DataModel } from "./_generated/dataModel";
+import { query } from "./_generated/server";
+
+// The component client has methods needed for integrating Convex with Better Auth,
+// as well as helper methods for general use.
+export const authComponent = createClient(components.betterAuth);
+const siteUrl = process.env.CONVEX_SITE_URL || "";
+
+export const createAuth = (
+ ctx: GenericCtx,
+ { optionsOnly } = { optionsOnly: false },
+) => {
+ return betterAuth({
+ baseURL: siteUrl,
+ // disable logging when createAuth is called just to generate options.
+ // this is not required, but there's a lot of noise in logs without it.
+ logger: {
+ disabled: optionsOnly,
+ },
+ trustedOrigins: ["your-scheme://", siteUrl],
+ database: authComponent.adapter(ctx),
+ // Configure simple, non-verified email/password to get started
+ emailAndPassword: {
+ enabled: true,
+ requireEmailVerification: false,
+ },
+ plugins: [
+ // The Expo and Convex plugins are required
+ expo(),
+ convex(),
+ ],
+ });
+};
+
+// Example function for getting the current user
+// Feel free to edit, omit, etc.
+export const getCurrentUser = query({
+ args: {},
+ handler: async (ctx) => {
+ return authComponent.getAuthUser(ctx);
+ },
+});
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/convex/convex.config.ts.ejs b/cli/src/templates/packages/convex/convex/convex.config.ts.ejs
new file mode 100644
index 00000000..a49fc735
--- /dev/null
+++ b/cli/src/templates/packages/convex/convex/convex.config.ts.ejs
@@ -0,0 +1,7 @@
+import { defineApp } from "convex/server";
+import betterAuth from "@convex-dev/better-auth/convex.config";
+
+const app = defineApp();
+app.use(betterAuth);
+
+export default app;
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/convex/http.ts.ejs b/cli/src/templates/packages/convex/convex/http.ts.ejs
new file mode 100644
index 00000000..e84c06a0
--- /dev/null
+++ b/cli/src/templates/packages/convex/convex/http.ts.ejs
@@ -0,0 +1,8 @@
+import { httpRouter } from "convex/server";
+import { authComponent, createAuth } from "./auth";
+
+const http = httpRouter();
+
+authComponent.registerRoutes(http, createAuth);
+
+export default http;
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/lib/auth-client.ts.ejs b/cli/src/templates/packages/convex/lib/auth-client.ts.ejs
new file mode 100644
index 00000000..ab3d25df
--- /dev/null
+++ b/cli/src/templates/packages/convex/lib/auth-client.ts.ejs
@@ -0,0 +1,17 @@
+import { createAuthClient } from "better-auth/react";
+import { convexClient } from "@convex-dev/better-auth/client/plugins";
+import { expoClient } from '@better-auth/expo/client';
+import Constants from 'expo-constants';
+import * as SecureStore from 'expo-secure-store';
+
+export const authClient = createAuthClient({
+ baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL,
+ plugins: [
+ expoClient({
+ scheme: Constants.expoConfig?.scheme as string,
+ storagePrefix: Constants.expoConfig?.scheme as string,
+ storage: SecureStore,
+ }),
+ convexClient(),
+ ],
+});
\ No newline at end of file
diff --git a/cli/src/templates/packages/convex/metro.config.js.ejs b/cli/src/templates/packages/convex/metro.config.js.ejs
new file mode 100644
index 00000000..351416f7
--- /dev/null
+++ b/cli/src/templates/packages/convex/metro.config.js.ejs
@@ -0,0 +1,16 @@
+const { getDefaultConfig } = require('expo/metro-config');
+<% if (props.stylingPackage?.name === "nativewind") { %>
+ const { withNativeWind } = require("nativewind/metro");
+<% } %>
+
+/** @type {import('expo/metro-config').MetroConfig} */
+// eslint-disable-next-line no-undef
+const config = getDefaultConfig(__dirname);
+
+config.resolver.unstable_enablePackageExports = true;
+
+<% if (props.stylingPackage?.name === "nativewind") { %>
+ module.exports = withNativeWind(config, { input: "./global.css" });
+<% } else { %>
+ module.exports = config;
+<% } %>
diff --git a/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs b/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs
index eac81954..52bdea6c 100644
--- a/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs
+++ b/cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs
@@ -1,7 +1,6 @@
<% if (props.stylingPackage?.name === "nativewind") { %>
import '../global.css';
<% } %>
-
<% if (props.stylingPackage?.name === "unistyles") { %>
import { useUnistyles } from "react-native-unistyles";
<% } %>
@@ -9,12 +8,22 @@
import '../translation';
<% } %>
import { Stack } from "expo-router";
-
<% if (props.analyticsPackage?.name === "vexo-analytics") { %>
import { vexo } from 'vexo-analytics';
vexo(process.env.EXPO_PUBLIC_VEXO_API_KEY);
<% } %>
+<% if (props.authenticationPackage?.name === "convex") { %>
+ import { ConvexReactClient } from "convex/react";
+ import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
+ import { authClient } from "@/lib/auth-client";
+
+ const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, {
+ // Optionally pause queries until the user is authenticated
+ expectAuth: true,
+ unsavedChangesWarning: false,
+ });
+<% } %>
export default function Layout() {
<% if (props.stylingPackage?.name === "unistyles") { %>
@@ -22,20 +31,40 @@ export default function Layout() {
<% } %>
return (
- <% if (props.stylingPackage?.name === "unistyles") { %>
-
- <% } else { %>
-
- <% } %>
+ <% if (props.authenticationPackage?.name === "convex") { %>
+
+ <% if (props.stylingPackage?.name === "unistyles") { %>
+
+ <% } else { %>
+
+ <% } %>
+
+ <% } else {%>
+ <% if (props.stylingPackage?.name === "unistyles") { %>
+
+ <% } else { %>
+
+ <% } %>
+ <%}%>
);
}
diff --git a/cli/src/templates/packages/react-navigation/App.tsx.ejs b/cli/src/templates/packages/react-navigation/App.tsx.ejs
index 5fe4be17..aafd2c1c 100644
--- a/cli/src/templates/packages/react-navigation/App.tsx.ejs
+++ b/cli/src/templates/packages/react-navigation/App.tsx.ejs
@@ -28,33 +28,59 @@ import "react-native-gesture-handler";
<% } else { %>
import Navigation from "./navigation";
<% } %>
+<% if (props.authenticationPackage?.name === "convex") { %>
+ import { ConvexReactClient } from "convex/react";
+ import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react";
+ import { authClient } from "@/lib/auth-client";
+
+ const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL as string, {
+ // Optionally pause queries until the user is authenticated
+ expectAuth: true,
+ unsavedChangesWarning: false,
+ });
+<% } %>
export default function App() {
- <% if (props.stylingPackage?.name === "unistyles") { %>
- const { theme, rt } = useUnistyles();
+ <% if (props.stylingPackage?.name === "unistyles") { %>
+ const { theme: uniTheme, rt } = useUnistyles();
const baseTheme = rt.colorScheme === 'dark' ? DarkTheme : DefaultTheme;
- const mergedTheme = useMemo(() => ({
+ const theme = useMemo(() => ({
...baseTheme,
colors: {
...baseTheme.colors,
- background: theme.colors.background,
- text: theme.colors.typography,
- primary: theme.colors.astral,
- secondary: theme.colors.cornflowerBlue,
- border: theme.colors.limedSpruce,
- card: theme.colors.background,
- notification: theme.colors.astral,
+ background: uniTheme.colors.background,
+ text: uniTheme.colors.typography,
+ primary: uniTheme.colors.astral,
+ secondary: uniTheme.colors.cornflowerBlue,
+ border: uniTheme.colors.limedSpruce,
+ card: uniTheme.colors.background,
+ notification: uniTheme.colors.astral,
},
- }), [baseTheme, theme.colors.background, theme.colors.typography, theme.colors.astral, theme.colors.cornflowerBlue, theme.colors.limedSpruce]);
-
- return ;
- <% } else if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet") { %>
- const colorScheme = useColorScheme();
+ }), [baseTheme, uniTheme.colors.background, uniTheme.colors.typography, uniTheme.colors.astral, uniTheme.colors.cornflowerBlue, uniTheme.colors.limedSpruce]);
+ <% } %>
+ <% if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet") { %>
+ const colorScheme = useColorScheme();
const theme = useMemo(() => colorScheme === 'dark' ? DarkTheme : DefaultTheme, [colorScheme]);
+ <% } %>
- return ;
- <% } else { %>
- return ;
- <% } %>
+ return (
+ <% if (props.stylingPackage?.name === "nativewind" || props.stylingPackage?.name === "stylesheet" || props.stylingPackage?.name === "unistyles") { %>
+ <% if (props.authenticationPackage?.name === "convex") { %>
+
+
+
+ <% } else {%>
+
+ <%}%>
+ <% } else { %>
+ <% if (props.authenticationPackage?.name === "convex") { %>
+
+
+
+ <% } else {%>
+
+ <%}%>
+ <% } %>
+ )
}
diff --git a/cli/src/types.ts b/cli/src/types.ts
index db4b548e..9c50efd9 100644
--- a/cli/src/types.ts
+++ b/cli/src/types.ts
@@ -24,13 +24,14 @@ export const availablePackages = [
'reactnavigation',
'stylesheet',
'supabase',
+ 'convex',
'unistyles',
'i18next',
'zustand',
'vexo-analytics'
] as const;
-export type AuthenticationSelect = 'supabase' | 'firebase' | undefined;
+export type AuthenticationSelect = 'supabase' | 'firebase' | 'convex' | undefined;
export type NavigationSelect = 'react-navigation' | 'expo-router' | undefined;
diff --git a/cli/src/utilities/configureProjectFiles.ts b/cli/src/utilities/configureProjectFiles.ts
index c1d016e9..4f0ab2e1 100644
--- a/cli/src/utilities/configureProjectFiles.ts
+++ b/cli/src/utilities/configureProjectFiles.ts
@@ -288,7 +288,7 @@ export function configureProjectFiles(
files = [...files, ...supabaseFiles];
}
- // add supabase files if needed
+ // add firebase files if needed
if (authenticationPackage?.name === 'firebase') {
const firebaseFiles = [
'packages/firebase/utils/firebase.ts.ejs',
@@ -299,6 +299,21 @@ export function configureProjectFiles(
files = [...files, ...firebaseFiles];
}
+ // add convex files if needed
+ if (authenticationPackage?.name === 'convex') {
+ const convexFiles = [
+ 'packages/convex/convex/auth.config.ts.ejs',
+ 'packages/convex/convex/auth.ts.ejs',
+ 'packages/convex/convex/http.ts.ejs',
+ 'packages/convex/convex/convex.config.ts.ejs',
+ 'packages/convex/lib/auth-client.ts.ejs',
+ 'packages/convex/.env.local.ejs',
+ 'packages/convex/metro.config.js.ejs'
+ ];
+
+ files = [...files, ...convexFiles];
+ }
+
// add vexo analytics files if needed
if (analyticsPackage?.name == 'vexo-analytics') {
const vexoFiles = ['packages/vexo-analytics/.env.ejs'];
diff --git a/cli/src/utilities/generateProjectFiles.ts b/cli/src/utilities/generateProjectFiles.ts
index e58439f3..a8a008e4 100644
--- a/cli/src/utilities/generateProjectFiles.ts
+++ b/cli/src/utilities/generateProjectFiles.ts
@@ -28,6 +28,10 @@ export function generateProjectFiles(
target = target.replace('packages/firebase/', '');
}
+ if (authenticationPackage?.name === 'convex') {
+ target = target.replace('packages/convex/', '');
+ }
+
//state management
if (stateManagementPackage?.name === 'zustand') {
target = target.replace('packages/zustand/', '');
diff --git a/cli/src/utilities/runCLI.ts b/cli/src/utilities/runCLI.ts
index bc338e9f..fcb4120d 100644
--- a/cli/src/utilities/runCLI.ts
+++ b/cli/src/utilities/runCLI.ts
@@ -353,7 +353,8 @@ export async function runCLI(toolbox: Toolbox, projectName: string): Promise = [
{ value: undefined, label: 'None' },
{ value: 'supabase', label: 'Supabase' },
- { value: 'firebase', label: 'Firebase' }
+ { value: 'firebase', label: 'Firebase' },
+ { value: 'convex', label: 'Convex + Better Auth' }
];
const authenticationSelect = await select({