Skip to content
5 changes: 5 additions & 0 deletions .changeset/sour-brooms-fail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-expo-stack': minor
---

Added Convex + Better Auth in the authentication choices
8 changes: 8 additions & 0 deletions cli/src/commands/create-expo-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
21 changes: 15 additions & 6 deletions cli/src/templates/base/.gitignore.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 29 additions & 7 deletions cli/src/templates/base/App.tsx.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
<% if (props.authenticationPackage?.name === "convex") { %>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
</ConvexBetterAuthProvider>
<% } else {%>
<>
<ScreenContent title="Home" path="App.tsx">
<% if (props.internalizationPackage?.name === "i18next") { %>
<InternalizationExample />
<% } %>
</ScreenContent>
<StatusBar style="auto" />
</>
<%}%>
);
}

2 changes: 1 addition & 1 deletion cli/src/templates/base/app.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions cli/src/templates/base/babel.config.js.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion cli/src/templates/base/eslint.config.js.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
11 changes: 11 additions & 0 deletions cli/src/templates/base/package.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion cli/src/templates/base/tsconfig.json.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -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/*"]
<% } %>
Expand Down
4 changes: 4 additions & 0 deletions cli/src/templates/packages/convex/.env.local.ejs
Original file line number Diff line number Diff line change
@@ -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=
8 changes: 8 additions & 0 deletions cli/src/templates/packages/convex/convex/auth.config.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default {
providers: [
{
domain: process.env.CONVEX_SITE_URL,
applicationID: "convex",
},
],
};
47 changes: 47 additions & 0 deletions cli/src/templates/packages/convex/convex/auth.ts.ejs
Original file line number Diff line number Diff line change
@@ -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<DataModel>(components.betterAuth);
const siteUrl = process.env.CONVEX_SITE_URL || "";

export const createAuth = (
ctx: GenericCtx<DataModel>,
{ 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);
},
});
7 changes: 7 additions & 0 deletions cli/src/templates/packages/convex/convex/convex.config.ts.ejs
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions cli/src/templates/packages/convex/convex/http.ts.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { httpRouter } from "convex/server";
import { authComponent, createAuth } from "./auth";

const http = httpRouter();

authComponent.registerRoutes(http, createAuth);

export default http;
17 changes: 17 additions & 0 deletions cli/src/templates/packages/convex/lib/auth-client.ts.ejs
Original file line number Diff line number Diff line change
@@ -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(),
],
});
16 changes: 16 additions & 0 deletions cli/src/templates/packages/convex/metro.config.js.ejs
Original file line number Diff line number Diff line change
@@ -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;
<% } %>
63 changes: 46 additions & 17 deletions cli/src/templates/packages/expo-router/stack/app/_layout.tsx.ejs
Original file line number Diff line number Diff line change
@@ -1,41 +1,70 @@
<% if (props.stylingPackage?.name === "nativewind") { %>
import '../global.css';
<% } %>

<% if (props.stylingPackage?.name === "unistyles") { %>
import { useUnistyles } from "react-native-unistyles";
<% } %>
<% if (props.internalizationPackage?.name === "i18next") { %>
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") { %>
const { theme } = useUnistyles();
<% } %>

return (
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
<% if (props.authenticationPackage?.name === "convex") { %>
<ConvexBetterAuthProvider client={convex} authClient={authClient}>
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
</ConvexBetterAuthProvider>
<% } else {%>
<% if (props.stylingPackage?.name === "unistyles") { %>
<Stack
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.background,
},
headerTitleStyle: {
color: theme.colors.typography,
},
headerTintColor: theme.colors.typography
}}
/>
<% } else { %>
<Stack />
<% } %>
<%}%>
);
}
Loading