diff --git a/.changeset/weak-carrots-return.md b/.changeset/weak-carrots-return.md
new file mode 100644
index 0000000..b2c1367
--- /dev/null
+++ b/.changeset/weak-carrots-return.md
@@ -0,0 +1,5 @@
+---
+"create-better-t-stack": minor
+---
+
+add expo with unistyles
diff --git a/apps/cli/README.md b/apps/cli/README.md
index 6680e4b..aac567c 100644
--- a/apps/cli/README.md
+++ b/apps/cli/README.md
@@ -23,21 +23,21 @@ Follow the prompts to configure your project or use the `--yes` flag for default
## Features
-| Category | Options |
-|----------|---------|
-| **TypeScript** | End-to-end type safety across all parts of your application |
-| **Frontend** | • React with TanStack Router
• React with React Router
• React with TanStack Start (SSR)
• Next.js
• SvelteKit
• Nuxt (Vue)
• SolidJS
• React Native with Expo
• None |
-| **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex |
-| **API Layer** | • tRPC (type-safe APIs)
• oRPC (OpenAPI-compatible type-safe APIs) |
-| **Runtime** | • Bun
• Node.js |
-| **Database** | • SQLite
• PostgreSQL
• MySQL
• MongoDB
• None |
-| **ORM** | • Drizzle (TypeScript-first)
• Prisma (feature-rich)
• Mongoose (for MongoDB)
• None |
-| **Database Setup** | • Turso (SQLite)
• Neon (PostgreSQL)
• Prisma Postgres (via Prisma Accelerate)
• MongoDB Atlas
• None (manual setup) |
-| **Authentication** | Better-Auth (email/password, with more options coming soon) |
-| **Styling** | Tailwind CSS with shadcn/ui components |
-| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds) |
-| **Examples** | • Todo app
• AI Chat interface (using Vercel AI SDK) |
-| **Developer Experience** | • Automatic Git initialization
• Package manager choice (npm, pnpm, bun)
• Automatic dependency installation |
+| Category | Options |
+| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **TypeScript** | End-to-end type safety across all parts of your application |
+| **Frontend** | • React with TanStack Router
• React with React Router
• React with TanStack Start (SSR)
• Next.js
• SvelteKit
• Nuxt (Vue)
• SolidJS
• React Native with NativeWind (via Expo)
• React Native with Unistyles (via Expo)
• None |
+| **Backend** | • Hono
• Express
• Elysia
• Next.js API routes
• Convex
• None |
+| **API Layer** | • tRPC (type-safe APIs)
• oRPC (OpenAPI-compatible type-safe APIs)
• None |
+| **Runtime** | • Bun
• Node.js |
+| **Database** | • SQLite
• PostgreSQL
• MySQL
• MongoDB
• None |
+| **ORM** | • Drizzle (TypeScript-first)
• Prisma (feature-rich)
• Mongoose (for MongoDB)
• None |
+| **Database Setup** | • Turso (SQLite)
• Neon (PostgreSQL)
• Prisma Postgres (via Prisma Accelerate)
• MongoDB Atlas
• None (manual setup) |
+| **Authentication** | Better-Auth (email/password, with more options coming soon) |
+| **Styling** | Tailwind CSS with shadcn/ui components |
+| **Addons** | • PWA support
• Tauri (desktop applications)
• Starlight (documentation site)
• Biome (linting and formatting)
• Husky (Git hooks)
• Turborepo (optimized builds) |
+| **Examples** | • Todo app
• AI Chat interface (using Vercel AI SDK) |
+| **Developer Experience** | • Automatic Git initialization
• Package manager choice (npm, pnpm, bun)
• Automatic dependency installation |
## Usage
@@ -51,7 +51,7 @@ Options:
--orm ORM type (none, drizzle, prisma, mongoose)
--auth Include authentication
--no-auth Exclude authentication
- --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native, none)
+ --frontend Frontend types (tanstack-router, react-router, tanstack-start, next, nuxt, svelte, solid, native-nativewind, native-unistyles, none)
--addons Additional addons (pwa, tauri, starlight, biome, husky, turborepo, none)
--examples Examples to include (todo, ai, none)
--git Initialize git repository
@@ -119,6 +119,7 @@ npx create-better-t-stack my-app --addons starlight
## Compatibility Notes
- **Convex backend**: Automatically disables authentication, database, ORM, and API options
+- **Backend 'none'**: If selected, this option will force related options like API, ORM, database, authentication, and runtime to 'none'. Examples will also be disabled (set to none/empty).
- **SvelteKit, Nuxt, and SolidJS** frontends are only compatible with oRPC API layer
- **PWA support** requires React with TanStack Router, React Router, or SolidJS
- **Tauri desktop app** requires React (TanStack Router/React Router), Nuxt, SvelteKit, or SolidJS
diff --git a/apps/cli/src/helpers/api-setup.ts b/apps/cli/src/helpers/api-setup.ts
index 15791e1..61d338f 100644
--- a/apps/cli/src/helpers/api-setup.ts
+++ b/apps/cli/src/helpers/api-setup.ts
@@ -125,7 +125,8 @@ export async function setupApi(config: ProjectConfig): Promise {
"tanstack-router",
"tanstack-start",
"next",
- "native",
+ "native-nativewind",
+ "native-unistyles",
];
const needsSolidQuery = frontend.includes("solid");
const needsReactQuery = frontend.some((f) => reactBasedFrontends.includes(f));
@@ -137,9 +138,14 @@ export async function setupApi(config: ProjectConfig): Promise {
];
const hasReactWeb = frontend.some(
- (f) => f !== "native" && reactBasedFrontends.includes(f),
+ (f) =>
+ f !== "native-nativewind" &&
+ f !== "native-unistyles" &&
+ reactBasedFrontends.includes(f),
);
- const hasNative = frontend.includes("native");
+ const hasNative =
+ frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles");
if (hasReactWeb && webDirExists) {
const webPkgJsonPath = path.join(webDir, "package.json");
diff --git a/apps/cli/src/helpers/auth-setup.ts b/apps/cli/src/helpers/auth-setup.ts
index cccecdc..fd20e52 100644
--- a/apps/cli/src/helpers/auth-setup.ts
+++ b/apps/cli/src/helpers/auth-setup.ts
@@ -45,7 +45,11 @@ export async function setupAuth(config: ProjectConfig): Promise {
});
}
- if (frontend.includes("native") && nativeDirExists) {
+ if (
+ (frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles")) &&
+ nativeDirExists
+ ) {
await addPackageDependency({
dependencies: ["better-auth", "@better-auth/expo"],
projectDir: nativeDir,
diff --git a/apps/cli/src/helpers/create-readme.ts b/apps/cli/src/helpers/create-readme.ts
index fd01e1a..0e235d0 100644
--- a/apps/cli/src/helpers/create-readme.ts
+++ b/apps/cli/src/helpers/create-readme.ts
@@ -39,7 +39,9 @@ function generateReadmeContent(options: ProjectConfig): string {
const isConvex = backend === "convex";
const hasReactRouter = frontend.includes("react-router");
const hasTanstackRouter = frontend.includes("tanstack-router");
- const hasNative = frontend.includes("native");
+ const hasNative =
+ frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
@@ -78,7 +80,16 @@ This project was created with [Better-T-Stack](https://github.com/AmanVarshney01
## Features
-${generateFeaturesList(database, auth, addons, orm, runtime, frontend, backend, api)}
+${generateFeaturesList(
+ database,
+ auth,
+ addons,
+ orm,
+ runtime,
+ frontend,
+ backend,
+ api,
+)}
## Getting Started
@@ -207,7 +218,9 @@ function generateFeaturesList(
const isConvex = backend === "convex";
const hasTanstackRouter = frontend.includes("tanstack-router");
const hasReactRouter = frontend.includes("react-router");
- const hasNative = frontend.includes("native");
+ const hasNative =
+ frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles");
const hasNext = frontend.includes("next");
const hasTanstackStart = frontend.includes("tanstack-start");
const hasSvelte = frontend.includes("svelte");
diff --git a/apps/cli/src/helpers/env-setup.ts b/apps/cli/src/helpers/env-setup.ts
index 2e09569..7aa961c 100644
--- a/apps/cli/src/helpers/env-setup.ts
+++ b/apps/cli/src/helpers/env-setup.ts
@@ -118,7 +118,10 @@ export async function setupEnvironmentVariables(
}
}
- if (frontend.includes("native")) {
+ if (
+ frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles")
+ ) {
const nativeDir = path.join(projectDir, "apps/native");
if (await fs.pathExists(nativeDir)) {
let envVarName = "EXPO_PUBLIC_SERVER_URL";
diff --git a/apps/cli/src/helpers/post-installation.ts b/apps/cli/src/helpers/post-installation.ts
index 1ef36bb..9a0f388 100644
--- a/apps/cli/src/helpers/post-installation.ts
+++ b/apps/cli/src/helpers/post-installation.ts
@@ -1,11 +1,6 @@
import { consola } from "consola";
import pc from "picocolors";
-import type {
- ProjectBackend,
- ProjectDatabase,
- ProjectOrm,
- ProjectRuntime,
-} from "../types";
+import type { ProjectDatabase, ProjectOrm, ProjectRuntime } from "../types";
import { getPackageExecutionCommand } from "../utils/get-package-execution-command";
import type { ProjectConfig } from "../types";
@@ -43,9 +38,11 @@ export function displayPostInstallInstructions(
const lintingInstructions = hasHuskyOrBiome
? getLintingInstructions(runCmd)
: "";
- const nativeInstructions = frontend?.includes("native")
- ? getNativeInstructions(isConvex)
- : "";
+ const nativeInstructions =
+ frontend?.includes("native-nativewind") ||
+ frontend?.includes("native-unistyles")
+ ? getNativeInstructions(isConvex)
+ : "";
const pwaInstructions =
addons?.includes("pwa") &&
(frontend?.includes("react-router") ||
@@ -67,7 +64,9 @@ export function displayPostInstallInstructions(
"solid",
].includes(f),
);
- const hasNative = frontend?.includes("native");
+ const hasNative =
+ frontend?.includes("native-nativewind") ||
+ frontend?.includes("native-unistyles");
const bunWebNativeWarning =
packageManager === "bun" && hasNative && hasWeb
@@ -90,7 +89,9 @@ export function displayPostInstallInstructions(
}
if (isConvex) {
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup ${pc.dim("(this will guide you through Convex project setup)")}\n`;
+ output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup ${pc.dim(
+ "(this will guide you through Convex project setup)",
+ )}\n`;
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
} else {
output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
@@ -101,7 +102,9 @@ export function displayPostInstallInstructions(
if (hasWeb) {
output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
} else if (!hasNative && !addons?.includes("starlight")) {
- output += `${pc.yellow("NOTE:")} You are creating a backend-only app (no frontend selected)\n`;
+ output += `${pc.yellow(
+ "NOTE:",
+ )} You are creating a backend-only app (no frontend selected)\n`;
}
if (!isConvex) {
@@ -122,8 +125,12 @@ export function displayPostInstallInstructions(
if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
- output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(tazeCommand)}\n\n`;
- output += `${pc.bold("Like Better-T Stack?")} Please consider giving us a star on GitHub:\n`;
+ output += `\n${pc.bold("Update all dependencies:\n")}${pc.cyan(
+ tazeCommand,
+ )}\n\n`;
+ output += `${pc.bold(
+ "Like Better-T Stack?",
+ )} Please consider giving us a star on GitHub:\n`;
output += pc.cyan("https://github.com/AmanVarshney01/create-better-t-stack");
consola.box(output);
@@ -183,7 +190,9 @@ function getDatabaseInstructions(
instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
if (database === "sqlite") {
instructions.push(
- `${pc.cyan("•")} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
+ `${pc.cyan(
+ "•",
+ )} Start local DB (if needed): ${`cd apps/server && ${runCmd} db:local`}`,
);
}
} else if (orm === "none") {
diff --git a/apps/cli/src/helpers/template-manager.ts b/apps/cli/src/helpers/template-manager.ts
index 2c17529..3936219 100644
--- a/apps/cli/src/helpers/template-manager.ts
+++ b/apps/cli/src/helpers/template-manager.ts
@@ -70,7 +70,9 @@ export async function setupFrontendTemplates(
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasSolidWeb = context.frontend.includes("solid");
- const hasNative = context.frontend.includes("native");
+ const hasNativeWind = context.frontend.includes("native-nativewind");
+ const hasUnistyles = context.frontend.includes("native-unistyles");
+ const hasNative = hasNativeWind || hasUnistyles;
const isConvex = context.backend === "convex";
if (hasReactWeb || hasNuxtWeb || hasSvelteWeb || hasSolidWeb) {
@@ -181,16 +183,45 @@ export async function setupFrontendTemplates(
}
}
- if (hasNative) {
+ if (hasNativeWind || hasUnistyles) {
const nativeAppDir = path.join(projectDir, "apps/native");
await fs.ensureDir(nativeAppDir);
- const nativeBaseDir = path.join(PKG_ROOT, "templates/frontend/native");
- if (await fs.pathExists(nativeBaseDir)) {
- await processAndCopyFiles("**/*", nativeBaseDir, nativeAppDir, context);
+ const nativeBaseCommonDir = path.join(
+ PKG_ROOT,
+ "templates/frontend/native/native-base",
+ );
+ if (await fs.pathExists(nativeBaseCommonDir)) {
+ await processAndCopyFiles(
+ "**/*",
+ nativeBaseCommonDir,
+ nativeAppDir,
+ context,
+ );
} else {
}
+ let nativeFrameworkPath = "";
+ if (hasNativeWind) {
+ nativeFrameworkPath = "nativewind";
+ } else if (hasUnistyles) {
+ nativeFrameworkPath = "unistyles";
+ }
+
+ const nativeSpecificDir = path.join(
+ PKG_ROOT,
+ `templates/frontend/native/${nativeFrameworkPath}`,
+ );
+ if (await fs.pathExists(nativeSpecificDir)) {
+ await processAndCopyFiles(
+ "**/*",
+ nativeSpecificDir,
+ nativeAppDir,
+ context,
+ true,
+ );
+ }
+
if (!isConvex && (context.api === "trpc" || context.api === "orpc")) {
const apiNativeSrcDir = path.join(
PKG_ROOT,
@@ -203,7 +234,6 @@ export async function setupFrontendTemplates(
nativeAppDir,
context,
);
- } else {
}
}
}
@@ -345,7 +375,9 @@ export async function setupAuthTemplate(
const hasNuxtWeb = context.frontend.includes("nuxt");
const hasSvelteWeb = context.frontend.includes("svelte");
const hasSolidWeb = context.frontend.includes("solid");
- const hasNative = context.frontend.includes("native");
+ const hasNativeWind = context.frontend.includes("native-nativewind");
+ const hasUnistyles = context.frontend.includes("native-unistyles");
+ const hasNative = hasNativeWind || hasUnistyles;
if (serverAppDirExists) {
const authServerBaseSrc = path.join(PKG_ROOT, "templates/auth/server/base");
@@ -475,10 +507,39 @@ export async function setupAuthTemplate(
}
if (hasNative && nativeAppDirExists) {
- const authNativeSrc = path.join(PKG_ROOT, "templates/auth/native");
- if (await fs.pathExists(authNativeSrc)) {
- await processAndCopyFiles("**/*", authNativeSrc, nativeAppDir, context);
- } else {
+ const authNativeBaseSrc = path.join(
+ PKG_ROOT,
+ "templates/auth/native/native-base",
+ );
+ if (await fs.pathExists(authNativeBaseSrc)) {
+ await processAndCopyFiles(
+ "**/*",
+ authNativeBaseSrc,
+ nativeAppDir,
+ context,
+ );
+ }
+
+ let nativeFrameworkAuthPath = "";
+ if (hasNativeWind) {
+ nativeFrameworkAuthPath = "nativewind";
+ } else if (hasUnistyles) {
+ nativeFrameworkAuthPath = "unistyles";
+ }
+
+ if (nativeFrameworkAuthPath) {
+ const authNativeFrameworkSrc = path.join(
+ PKG_ROOT,
+ `templates/auth/native/${nativeFrameworkAuthPath}`,
+ );
+ if (await fs.pathExists(authNativeFrameworkSrc)) {
+ await processAndCopyFiles(
+ "**/*",
+ authNativeFrameworkSrc,
+ nativeAppDir,
+ context,
+ );
+ }
}
}
}
@@ -695,6 +756,9 @@ export async function handleExtras(
context: ProjectConfig,
): Promise {
const extrasDir = path.join(PKG_ROOT, "templates/extras");
+ const hasNativeWind = context.frontend.includes("native-nativewind");
+ const hasUnistyles = context.frontend.includes("native-unistyles");
+ const hasNative = hasNativeWind || hasUnistyles;
if (context.packageManager === "pnpm") {
const pnpmWorkspaceSrc = path.join(extrasDir, "pnpm-workspace.yaml");
@@ -706,7 +770,7 @@ export async function handleExtras(
if (
context.packageManager === "pnpm" &&
- (context.frontend.includes("native") || context.frontend.includes("nuxt"))
+ (hasNative || context.frontend.includes("nuxt"))
) {
const npmrcTemplateSrc = path.join(extrasDir, "_npmrc.hbs");
const npmrcDest = path.join(projectDir, ".npmrc");
diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts
index a039652..7d684be 100644
--- a/apps/cli/src/index.ts
+++ b/apps/cli/src/index.ts
@@ -84,7 +84,8 @@ async function main() {
"tanstack-start",
"next",
"nuxt",
- "native",
+ "native-nativewind",
+ "native-unistyles",
"svelte",
"solid",
"none",
@@ -303,6 +304,9 @@ async function main() {
config.runtime = "none";
config.dbSetup = "none";
config.examples = ["todo"];
+ log.info(
+ "Due to '--backend convex' flag, the following options have been automatically set: auth=false, database=none, orm=none, api=none, runtime=none, dbSetup=none, examples=todo",
+ );
} else if (config.backend === "none") {
config.auth = false;
config.database = "none";
@@ -311,10 +315,24 @@ async function main() {
config.runtime = "none";
config.dbSetup = "none";
config.examples = [];
+ log.info(
+ "Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
+ );
} else if (config.database === "none") {
config.orm = "none";
+ log.info(
+ "Due to '--database none', '--orm' has been automatically set to 'none'.",
+ );
+
config.auth = false;
+ log.info(
+ "Due to '--database none', '--auth' has been automatically set to 'false'.",
+ );
+
config.dbSetup = "none";
+ log.info(
+ "Due to '--database none', '--db-setup' has been automatically set to 'none'.",
+ );
}
log.info(
@@ -380,13 +398,16 @@ function processAndValidateFlags(
if (options.api) {
config.api = options.api as ProjectApi;
if (options.api === "none") {
- if (options.backend && options.backend !== "convex") {
+ if (
+ options.backend &&
+ options.backend !== "convex" &&
+ options.backend !== "none"
+ ) {
consola.fatal(
- `'--api none' is only supported with '--backend convex'. Please choose a different API setting or use '--backend convex'.`,
+ `'--api none' is only supported with '--backend convex' or '--backend none'. Please choose a different API setting or use '--backend convex' or '--backend none'.`,
);
process.exit(1);
}
- config.backend = "convex";
}
}
@@ -468,12 +489,22 @@ function processAndValidateFlags(
f === "svelte" ||
f === "solid",
);
+ const nativeFrontends = validOptions.filter(
+ (f) => f === "native-nativewind" || f === "native-unistyles",
+ );
+
if (webFrontends.length > 1) {
consola.fatal(
"Cannot select multiple web frameworks. Choose only one of: tanstack-router, tanstack-start, react-router, next, nuxt, svelte, solid",
);
process.exit(1);
}
+ if (nativeFrontends.length > 1) {
+ consola.fatal(
+ "Cannot select multiple native frameworks. Choose only one of: native-nativewind, native-unistyles",
+ );
+ process.exit(1);
+ }
config.frontend = validOptions;
}
}
@@ -595,6 +626,9 @@ function processAndValidateFlags(
process.exit(1);
}
config.examples = [];
+ log.info(
+ "Due to '--backend none', the following options have been automatically set: --auth=false, --database=none, --orm=none, --api=none, --runtime=none, --db-setup=none, --examples=none",
+ );
} else {
const effectiveDatabase =
config.database ?? (options.yes ? DEFAULT_CONFIG.database : undefined);
@@ -621,6 +655,9 @@ function processAndValidateFlags(
process.exit(1);
}
config.orm = "none";
+ log.info(
+ "Due to '--database none', '--orm' has been automatically set to 'none'.",
+ );
if (providedFlags.has("auth") && options.auth === true) {
consola.fatal(
@@ -629,6 +666,9 @@ function processAndValidateFlags(
process.exit(1);
}
config.auth = false;
+ log.info(
+ "Due to '--database none', '--auth' has been automatically set to 'false'.",
+ );
if (providedFlags.has("dbSetup") && options.dbSetup !== "none") {
consola.fatal(
@@ -637,6 +677,9 @@ function processAndValidateFlags(
process.exit(1);
}
config.dbSetup = "none";
+ log.info(
+ "Due to '--database none', '--db-setup' has been automatically set to 'none'.",
+ );
}
if (config.orm === "mongoose" && !providedFlags.has("database")) {
@@ -753,6 +796,9 @@ function processAndValidateFlags(
) {
if (config.api !== "none") {
config.api = "orpc";
+ log.info(
+ `Due to frontend selection, API has been set to 'orpc'. tRPC is not compatible with Nuxt, Svelte, or Solid Framework`,
+ );
}
}
@@ -813,7 +859,8 @@ function processAndValidateFlags(
const onlyNativeFrontend =
effectiveFrontend &&
effectiveFrontend.length === 1 &&
- effectiveFrontend[0] === "native";
+ (effectiveFrontend[0] === "native-nativewind" ||
+ effectiveFrontend[0] === "native-unistyles");
if (
onlyNativeFrontend &&
@@ -822,7 +869,7 @@ function processAndValidateFlags(
!config.examples.includes("none")
) {
consola.fatal(
- "Examples are not supported when only the 'native' frontend is selected.",
+ "Examples are not supported when only a native frontend (NativeWind or Unistyles) is selected.",
);
process.exit(1);
}
diff --git a/apps/cli/src/prompts/backend-framework.ts b/apps/cli/src/prompts/backend-framework.ts
index 404bfbe..355948a 100644
--- a/apps/cli/src/prompts/backend-framework.ts
+++ b/apps/cli/src/prompts/backend-framework.ts
@@ -48,7 +48,6 @@ export async function getBackendFrameworkChoice(
});
}
- // Add "None" option
backendOptions.push({
value: "none" as const,
label: "None",
diff --git a/apps/cli/src/prompts/examples.ts b/apps/cli/src/prompts/examples.ts
index 3f7d294..ec555b1 100644
--- a/apps/cli/src/prompts/examples.ts
+++ b/apps/cli/src/prompts/examples.ts
@@ -27,7 +27,10 @@ export async function getExamplesChoice(
if (database === "none") return [];
const onlyNative =
- frontends && frontends.length === 1 && frontends[0] === "native";
+ frontends &&
+ frontends.length === 1 &&
+ (frontends[0] === "native-nativewind" ||
+ frontends[0] === "native-unistyles");
if (onlyNative) {
return [];
}
diff --git a/apps/cli/src/prompts/frontend-option.ts b/apps/cli/src/prompts/frontend-option.ts
index e7adebf..32ac209 100644
--- a/apps/cli/src/prompts/frontend-option.ts
+++ b/apps/cli/src/prompts/frontend-option.ts
@@ -95,7 +95,28 @@ export async function getFrontendChoice(
}
if (frontendTypes.includes("native")) {
- result.push("native");
+ const nativeFramework = await select({
+ message: "Choose native framework",
+ options: [
+ {
+ value: "native-nativewind" as const,
+ label: "NativeWind",
+ hint: "Use Tailwind CSS for React Native",
+ },
+ {
+ value: "native-unistyles" as const,
+ label: "Unistyles",
+ hint: "Consistent styling for React Native",
+ },
+ ],
+ initialValue: "native-nativewind",
+ });
+
+ if (isCancel(nativeFramework)) {
+ cancel(pc.red("Operation cancelled"));
+ process.exit(0);
+ }
+ result.push(nativeFramework);
}
return result;
diff --git a/apps/cli/src/types.ts b/apps/cli/src/types.ts
index 4837138..d123dc9 100644
--- a/apps/cli/src/types.ts
+++ b/apps/cli/src/types.ts
@@ -29,7 +29,8 @@ export type ProjectFrontend =
| "tanstack-start"
| "next"
| "nuxt"
- | "native"
+ | "native-nativewind"
+ | "native-unistyles"
| "svelte"
| "solid"
| "none";
diff --git a/apps/cli/templates/auth/native/lib/auth-client.ts b/apps/cli/templates/auth/native/native-base/lib/auth-client.ts.hbs
similarity index 100%
rename from apps/cli/templates/auth/native/lib/auth-client.ts
rename to apps/cli/templates/auth/native/native-base/lib/auth-client.ts.hbs
diff --git a/apps/cli/templates/auth/native/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/native/nativewind/app/(drawer)/index.tsx.hbs
similarity index 100%
rename from apps/cli/templates/auth/native/app/(drawer)/index.tsx.hbs
rename to apps/cli/templates/auth/native/nativewind/app/(drawer)/index.tsx.hbs
diff --git a/apps/cli/templates/auth/native/components/sign-in.tsx.hbs b/apps/cli/templates/auth/native/nativewind/components/sign-in.tsx.hbs
similarity index 100%
rename from apps/cli/templates/auth/native/components/sign-in.tsx.hbs
rename to apps/cli/templates/auth/native/nativewind/components/sign-in.tsx.hbs
diff --git a/apps/cli/templates/auth/native/components/sign-up.tsx.hbs b/apps/cli/templates/auth/native/nativewind/components/sign-up.tsx.hbs
similarity index 100%
rename from apps/cli/templates/auth/native/components/sign-up.tsx.hbs
rename to apps/cli/templates/auth/native/nativewind/components/sign-up.tsx.hbs
diff --git a/apps/cli/templates/auth/native/unistyles/app/(drawer)/index.tsx.hbs b/apps/cli/templates/auth/native/unistyles/app/(drawer)/index.tsx.hbs
new file mode 100644
index 0000000..b7c6193
--- /dev/null
+++ b/apps/cli/templates/auth/native/unistyles/app/(drawer)/index.tsx.hbs
@@ -0,0 +1,179 @@
+import { authClient } from "@/lib/auth-client";
+import { useQuery } from "@tanstack/react-query";
+import { ScrollView, Text, TouchableOpacity, View } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+import { Container } from "@/components/container";
+import { SignIn } from "@/components/sign-in";
+import { SignUp } from "@/components/sign-up";
+{{#if (eq api "orpc")}}
+import { queryClient, orpc } from "@/utils/orpc";
+{{/if}}
+{{#if (eq api "trpc")}}
+import { queryClient, trpc } from "@/utils/trpc";
+{{/if}}
+
+export default function Home() {
+ {{#if (eq api "orpc")}}
+ const healthCheck = useQuery(orpc.healthCheck.queryOptions());
+ const privateData = useQuery(orpc.privateData.queryOptions());
+ {{/if}}
+ {{#if (eq api "trpc")}}
+ const healthCheck = useQuery(trpc.healthCheck.queryOptions());
+ const privateData = useQuery(trpc.privateData.queryOptions());
+ {{/if}}
+ const { data: session } = authClient.useSession();
+
+ return (
+
+
+
+ BETTER T STACK
+ {session?.user ? (
+
+
+
+ Welcome,{" "}
+ {session.user.name}
+
+
+ {session.user.email}
+
+ {
+ authClient.signOut();
+ queryClient.invalidateQueries();
+ }}
+ >
+ Sign Out
+
+
+ ) : null}
+
+ API Status
+
+
+
+ {healthCheck.isLoading
+ ? "Checking..."
+ : healthCheck.data
+ ? "Connected to API"
+ : "API Disconnected"}
+
+
+
+
+ Private Data
+ {privateData && (
+
+
+ {privateData.data?.message}
+
+
+ )}
+
+ {!session?.user && (
+ <>
+
+
+ >
+ )}
+
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ pageContainer: {
+ paddingHorizontal: 8,
+ },
+ headerTitle: {
+ color: theme?.colors?.typography,
+ fontSize: 30,
+ fontWeight: "bold",
+ marginBottom: 16,
+ },
+ sessionInfoCard: {
+ marginBottom: 24,
+ padding: 16,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme?.colors?.border,
+ },
+ sessionUserRow: {
+ flexDirection: "row",
+ justifyContent: "space-between",
+ alignItems: "center",
+ marginBottom: 8,
+ },
+ welcomeText: {
+ color: theme?.colors?.typography,
+ fontSize: 16,
+ },
+ userNameText: {
+ fontWeight: "500",
+ color: theme?.colors?.typography,
+ },
+ emailText: {
+ color: theme?.colors?.typography,
+ fontSize: 14,
+ marginBottom: 16,
+ },
+ signOutButton: {
+ backgroundColor: theme?.colors?.destructive,
+ paddingVertical: 8,
+ paddingHorizontal: 16,
+ borderRadius: 6,
+ alignSelf: "flex-start",
+ },
+ signOutButtonText: {
+ fontWeight: "500",
+ },
+ apiStatusCard: {
+ marginBottom: 24,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme?.colors?.border,
+ padding: 16,
+ },
+ cardTitle: {
+ marginBottom: 12,
+ fontWeight: "500",
+ color: theme?.colors?.typography,
+ },
+ apiStatusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ statusIndicatorDot: {
+ height: 12,
+ width: 12,
+ borderRadius: 9999,
+ },
+ statusIndicatorGreen: {
+ backgroundColor: theme.colors.success,
+ },
+ statusIndicatorRed: {
+ backgroundColor: theme.colors.destructive,
+ },
+ mutedText: {
+ color: theme?.colors?.typography,
+ },
+ privateDataCard: {
+ marginBottom: 24,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme?.colors?.border,
+ padding: 16,
+ },
+}));
diff --git a/apps/cli/templates/auth/native/unistyles/components/sign-in.tsx.hbs b/apps/cli/templates/auth/native/unistyles/components/sign-in.tsx.hbs
new file mode 100644
index 0000000..c63395b
--- /dev/null
+++ b/apps/cli/templates/auth/native/unistyles/components/sign-in.tsx.hbs
@@ -0,0 +1,134 @@
+import { authClient } from "@/lib/auth-client";
+{{#if (eq api "trpc")}}
+import { queryClient } from "@/utils/trpc";
+{{/if}}
+{{#if (eq api "orpc")}}
+import { queryClient } from "@/utils/orpc";
+{{/if}}
+import { useState } from "react";
+import {
+ ActivityIndicator,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+export function SignIn() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleLogin = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ await authClient.signIn.email(
+ {
+ email,
+ password,
+ },
+ {
+ onError: (error) => {
+ setError(error.error?.message || "Failed to sign in");
+ setIsLoading(false);
+ },
+ onSuccess: () => {
+ setEmail("");
+ setPassword("");
+ queryClient.refetchQueries();
+ },
+ onFinished: () => {
+ setIsLoading(false);
+ },
+ },
+ );
+ };
+
+ return (
+
+ Sign In
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ Sign In
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ container: {
+ marginTop: 24,
+ padding: 16,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: theme.colors.typography,
+ marginBottom: 16,
+ },
+ errorContainer: {
+ marginBottom: 16,
+ padding: 12,
+ borderRadius: 6,
+ },
+ errorText: {
+ color: theme.colors.destructive,
+ fontSize: 14,
+ },
+ input: {
+ marginBottom: 12,
+ padding: 16,
+ borderRadius: 6,
+ color: theme.colors.typography,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ button: {
+ backgroundColor: theme.colors.primary,
+ padding: 16,
+ borderRadius: 6,
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ buttonText: {
+ fontWeight: "500",
+ },
+}));
diff --git a/apps/cli/templates/auth/native/unistyles/components/sign-up.tsx.hbs b/apps/cli/templates/auth/native/unistyles/components/sign-up.tsx.hbs
new file mode 100644
index 0000000..67f0a1b
--- /dev/null
+++ b/apps/cli/templates/auth/native/unistyles/components/sign-up.tsx.hbs
@@ -0,0 +1,152 @@
+import { authClient } from "@/lib/auth-client";
+{{#if (eq api "trpc")}}
+import { queryClient } from "@/utils/trpc";
+{{/if}}
+{{#if (eq api "orpc")}}
+import { queryClient } from "@/utils/orpc";
+{{/if}}
+import { useState } from "react";
+import {
+ ActivityIndicator,
+ Text,
+ TextInput,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+export function SignUp() {
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const handleSignUp = async () => {
+ setIsLoading(true);
+ setError(null);
+
+ await authClient.signUp.email(
+ {
+ name,
+ email,
+ password,
+ },
+ {
+ onError: (error) => {
+ setError(error.error?.message || "Failed to sign up");
+ setIsLoading(false);
+ },
+ onSuccess: () => {
+ setName("");
+ setEmail("");
+ setPassword("");
+ queryClient.refetchQueries();
+ },
+ onFinished: () => {
+ setIsLoading(false);
+ },
+ },
+ );
+ };
+
+ return (
+
+ Create Account
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+ ) : (
+ Sign Up
+ )}
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ container: {
+ marginTop: 24,
+ padding: 16,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ title: {
+ fontSize: 18,
+ fontWeight: "600",
+ color: theme.colors.typography,
+ marginBottom: 16,
+ },
+ errorContainer: {
+ marginBottom: 16,
+ padding: 12,
+ borderRadius: 6,
+ },
+ errorText: {
+ color: theme.colors.destructive,
+ fontSize: 14,
+ },
+ input: {
+ marginBottom: 12,
+ padding: 16,
+ borderRadius: 6,
+ color: theme.colors.typography,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ inputLast: {
+ marginBottom: 16,
+ padding: 16,
+ borderRadius: 6,
+ color: theme.colors.typography,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ },
+ button: {
+ backgroundColor: theme.colors.primary,
+ padding: 16,
+ borderRadius: 6,
+ flexDirection: "row",
+ justifyContent: "center",
+ alignItems: "center",
+ },
+ buttonText: {
+ fontWeight: "500",
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/assets/adaptive-icon.png b/apps/cli/templates/frontend/native/native-base/assets/adaptive-icon.png
similarity index 100%
rename from apps/cli/templates/frontend/native/assets/adaptive-icon.png
rename to apps/cli/templates/frontend/native/native-base/assets/adaptive-icon.png
diff --git a/apps/cli/templates/frontend/native/assets/favicon.png b/apps/cli/templates/frontend/native/native-base/assets/favicon.png
similarity index 100%
rename from apps/cli/templates/frontend/native/assets/favicon.png
rename to apps/cli/templates/frontend/native/native-base/assets/favicon.png
diff --git a/apps/cli/templates/frontend/native/assets/icon.png b/apps/cli/templates/frontend/native/native-base/assets/icon.png
similarity index 100%
rename from apps/cli/templates/frontend/native/assets/icon.png
rename to apps/cli/templates/frontend/native/native-base/assets/icon.png
diff --git a/apps/cli/templates/frontend/native/assets/splash.png b/apps/cli/templates/frontend/native/native-base/assets/splash.png
similarity index 100%
rename from apps/cli/templates/frontend/native/assets/splash.png
rename to apps/cli/templates/frontend/native/native-base/assets/splash.png
diff --git a/apps/cli/templates/frontend/native/_gitignore b/apps/cli/templates/frontend/native/nativewind/_gitignore
similarity index 100%
rename from apps/cli/templates/frontend/native/_gitignore
rename to apps/cli/templates/frontend/native/nativewind/_gitignore
diff --git a/apps/cli/templates/frontend/native/app-env.d.ts b/apps/cli/templates/frontend/native/nativewind/app-env.d.ts
similarity index 100%
rename from apps/cli/templates/frontend/native/app-env.d.ts
rename to apps/cli/templates/frontend/native/nativewind/app-env.d.ts
diff --git a/apps/cli/templates/frontend/native/app.json b/apps/cli/templates/frontend/native/nativewind/app.json
similarity index 100%
rename from apps/cli/templates/frontend/native/app.json
rename to apps/cli/templates/frontend/native/nativewind/app.json
diff --git a/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/(drawer)/(tabs)/_layout.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/_layout.tsx
diff --git a/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/index.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/(drawer)/(tabs)/index.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/index.tsx
diff --git a/apps/cli/templates/frontend/native/app/(drawer)/(tabs)/two.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/(drawer)/(tabs)/two.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/(drawer)/(tabs)/two.tsx
diff --git a/apps/cli/templates/frontend/native/app/(drawer)/_layout.tsx b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/(drawer)/_layout.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/(drawer)/_layout.tsx
diff --git a/apps/cli/templates/frontend/native/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs
similarity index 100%
rename from apps/cli/templates/frontend/native/app/(drawer)/index.tsx.hbs
rename to apps/cli/templates/frontend/native/nativewind/app/(drawer)/index.tsx.hbs
diff --git a/apps/cli/templates/frontend/native/app/+html.tsx b/apps/cli/templates/frontend/native/nativewind/app/+html.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/+html.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/+html.tsx
diff --git a/apps/cli/templates/frontend/native/app/+not-found.tsx b/apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/+not-found.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/+not-found.tsx
diff --git a/apps/cli/templates/frontend/native/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs
similarity index 100%
rename from apps/cli/templates/frontend/native/app/_layout.tsx.hbs
rename to apps/cli/templates/frontend/native/nativewind/app/_layout.tsx.hbs
diff --git a/apps/cli/templates/frontend/native/app/modal.tsx b/apps/cli/templates/frontend/native/nativewind/app/modal.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/app/modal.tsx
rename to apps/cli/templates/frontend/native/nativewind/app/modal.tsx
diff --git a/apps/cli/templates/frontend/native/babel.config.js b/apps/cli/templates/frontend/native/nativewind/babel.config.js
similarity index 100%
rename from apps/cli/templates/frontend/native/babel.config.js
rename to apps/cli/templates/frontend/native/nativewind/babel.config.js
diff --git a/apps/cli/templates/frontend/native/components/container.tsx b/apps/cli/templates/frontend/native/nativewind/components/container.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/components/container.tsx
rename to apps/cli/templates/frontend/native/nativewind/components/container.tsx
diff --git a/apps/cli/templates/frontend/native/components/header-button.tsx b/apps/cli/templates/frontend/native/nativewind/components/header-button.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/components/header-button.tsx
rename to apps/cli/templates/frontend/native/nativewind/components/header-button.tsx
diff --git a/apps/cli/templates/frontend/native/components/tabbar-icon.tsx b/apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/components/tabbar-icon.tsx
rename to apps/cli/templates/frontend/native/nativewind/components/tabbar-icon.tsx
diff --git a/apps/cli/templates/frontend/native/global.css b/apps/cli/templates/frontend/native/nativewind/global.css
similarity index 100%
rename from apps/cli/templates/frontend/native/global.css
rename to apps/cli/templates/frontend/native/nativewind/global.css
diff --git a/apps/cli/templates/frontend/native/lib/android-navigation-bar.tsx b/apps/cli/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx
similarity index 100%
rename from apps/cli/templates/frontend/native/lib/android-navigation-bar.tsx
rename to apps/cli/templates/frontend/native/nativewind/lib/android-navigation-bar.tsx
diff --git a/apps/cli/templates/frontend/native/lib/constants.ts b/apps/cli/templates/frontend/native/nativewind/lib/constants.ts
similarity index 100%
rename from apps/cli/templates/frontend/native/lib/constants.ts
rename to apps/cli/templates/frontend/native/nativewind/lib/constants.ts
diff --git a/apps/cli/templates/frontend/native/lib/use-color-scheme.ts b/apps/cli/templates/frontend/native/nativewind/lib/use-color-scheme.ts
similarity index 100%
rename from apps/cli/templates/frontend/native/lib/use-color-scheme.ts
rename to apps/cli/templates/frontend/native/nativewind/lib/use-color-scheme.ts
diff --git a/apps/cli/templates/frontend/native/metro.config.js b/apps/cli/templates/frontend/native/nativewind/metro.config.js
similarity index 100%
rename from apps/cli/templates/frontend/native/metro.config.js
rename to apps/cli/templates/frontend/native/nativewind/metro.config.js
diff --git a/apps/cli/templates/frontend/native/package.json b/apps/cli/templates/frontend/native/nativewind/package.json
similarity index 100%
rename from apps/cli/templates/frontend/native/package.json
rename to apps/cli/templates/frontend/native/nativewind/package.json
diff --git a/apps/cli/templates/frontend/native/tailwind.config.js b/apps/cli/templates/frontend/native/nativewind/tailwind.config.js
similarity index 100%
rename from apps/cli/templates/frontend/native/tailwind.config.js
rename to apps/cli/templates/frontend/native/nativewind/tailwind.config.js
diff --git a/apps/cli/templates/frontend/native/tsconfig.json b/apps/cli/templates/frontend/native/nativewind/tsconfig.json
similarity index 100%
rename from apps/cli/templates/frontend/native/tsconfig.json
rename to apps/cli/templates/frontend/native/nativewind/tsconfig.json
diff --git a/apps/cli/templates/frontend/native/unistyles/_gitignore b/apps/cli/templates/frontend/native/unistyles/_gitignore
new file mode 100644
index 0000000..2ff55da
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/_gitignore
@@ -0,0 +1,24 @@
+node_modules/
+.expo/
+dist/
+npm-debug.*
+*.jks
+*.p8
+*.p12
+*.key
+*.mobileprovision
+*.orig.*
+web-build/
+# expo router
+expo-env.d.ts
+
+
+
+ios
+android
+
+# macOS
+.DS_Store
+
+# Temporary files created by Metro to check the health of the file watcher
+.metro-health-check*
\ No newline at end of file
diff --git a/apps/cli/templates/frontend/native/unistyles/app.json b/apps/cli/templates/frontend/native/unistyles/app.json
new file mode 100644
index 0000000..d2856dc
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app.json
@@ -0,0 +1,44 @@
+{
+ "expo": {
+ "name": "my-better-t-app",
+ "slug": "my-better-t-app",
+ "version": "1.0.0",
+ "newArchEnabled": true,
+ "scheme": "my-better-t-app",
+ "web": {
+ "bundler": "metro",
+ "output": "static",
+ "favicon": "./assets/favicon.png"
+ },
+ "plugins": [
+ "expo-router",
+ "react-native-edge-to-edge",
+ "expo-secure-store"
+ ],
+ "experiments": {
+ "typedRoutes": true,
+ "tsconfigPaths": true,
+ "reactCompiler": true
+ },
+ "orientation": "portrait",
+ "icon": "./assets/icon.png",
+ "userInterfaceStyle": "automatic",
+ "splash": {
+ "image": "./assets/splash.png",
+ "resizeMode": "contain",
+ "backgroundColor": "#ffffff"
+ },
+ "assetBundlePatterns": ["**/*"],
+ "ios": {
+ "supportsTablet": true,
+ "bundleIdentifier": "com.amanvarshney01.mybettertapp"
+ },
+ "android": {
+ "adaptiveIcon": {
+ "foregroundImage": "./assets/adaptive-icon.png",
+ "backgroundColor": "#ffffff"
+ },
+ "package": "com.amanvarshney01.mybettertapp"
+ }
+ }
+}
diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx
new file mode 100644
index 0000000..e4a5e2b
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/_layout.tsx
@@ -0,0 +1,34 @@
+import { Tabs } from "expo-router";
+import { useUnistyles } from "react-native-unistyles";
+
+import { TabBarIcon } from "@/components/tabbar-icon";
+
+export default function TabLayout() {
+ const { theme } = useUnistyles();
+
+ return (
+
+ ,
+ }}
+ />
+ ,
+ }}
+ />
+
+ );
+}
diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx
new file mode 100644
index 0000000..52f73f4
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/index.tsx
@@ -0,0 +1,29 @@
+import { Stack } from "expo-router";
+import { StyleSheet } from "react-native-unistyles";
+import { Container } from "@/components/container";
+import { Text, View } from "react-native";
+
+export default function Home() {
+ return (
+ <>
+
+
+
+ Tab One
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ text: {
+ color: theme.colors.typography,
+ },
+ container: {
+ flex: 1,
+ paddingBottom: 100,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx
new file mode 100644
index 0000000..b75640e
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/(tabs)/two.tsx
@@ -0,0 +1,29 @@
+import { Stack } from "expo-router";
+import { StyleSheet } from "react-native-unistyles";
+import { Container } from "@/components/container";
+import { Text, View } from "react-native";
+
+export default function Home() {
+ return (
+ <>
+
+
+
+ Tab Two
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ text: {
+ color: theme.colors.typography,
+ },
+ container: {
+ flex: 1,
+ paddingBottom: 100,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx
new file mode 100644
index 0000000..fc0d630
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/_layout.tsx
@@ -0,0 +1,59 @@
+import { Ionicons, MaterialIcons } from "@expo/vector-icons";
+import { Link } from "expo-router";
+import { Drawer } from "expo-router/drawer";
+import { useUnistyles } from "react-native-unistyles";
+
+import { HeaderButton } from "../../components/header-button";
+
+const DrawerLayout = () => {
+ const { theme } = useUnistyles();
+
+ return (
+
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ headerRight: () => (
+
+
+
+ ),
+ }}
+ />
+
+ );
+};
+
+export default DrawerLayout;
diff --git a/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs
new file mode 100644
index 0000000..3eca269
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/(drawer)/index.tsx.hbs
@@ -0,0 +1,115 @@
+import { ScrollView, Text, View } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+import { Container } from "@/components/container";
+
+{{#if (eq api "orpc")}}
+import { useQuery } from "@tanstack/react-query";
+import { orpc } from "@/utils/orpc";
+{{/if}}
+{{#if (eq api "trpc")}}
+import { useQuery } from "@tanstack/react-query";
+import { trpc } from "@/utils/trpc";
+{{/if}}
+{{#if (eq backend "convex")}}
+import { useQuery } from "convex/react";
+import { api } from "@{{ projectName }}/backend/convex/_generated/api.js";
+{{/if}}
+
+export default function Home() {
+ {{#if (eq api "orpc")}}
+ const healthCheck = useQuery(orpc.healthCheck.queryOptions());
+ {{/if}}
+ {{#if (eq api "trpc")}}
+ const healthCheck = useQuery(trpc.healthCheck.queryOptions());
+ {{/if}}
+ {{#if (eq backend "convex")}}
+ const healthCheck = useQuery(api.healthCheck.get);
+ {{/if}}
+
+ return (
+
+
+ BETTER T STACK
+
+
+ API Status
+
+
+
+ {{#if (or (eq api "orpc") (eq api "trpc"))}}
+ {healthCheck.isLoading
+ ? "Checking..."
+ : healthCheck.data
+ ? "Connected"
+ : "Disconnected"}
+ {{/if}}
+ {{#if (eq backend "convex")}}
+ {healthCheck === undefined
+ ? "Checking..."
+ : healthCheck === "OK"
+ ? "Connected"
+ : "Error"}
+ {{/if}}
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ pageContainer: {
+ paddingHorizontal: 8,
+ },
+ headerTitle: {
+ color: theme?.colors?.typography,
+ fontSize: 30,
+ fontWeight: "bold",
+ marginBottom: 16,
+ },
+ apiStatusCard: {
+ marginBottom: 24,
+ borderRadius: 8,
+ borderWidth: 1,
+ borderColor: theme?.colors?.border,
+ padding: 16,
+ },
+ cardTitle: {
+ marginBottom: 12,
+ fontWeight: "500",
+ color: theme?.colors?.typography,
+ },
+ apiStatusRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ gap: 8,
+ },
+ statusIndicatorDot: {
+ height: 12,
+ width: 12,
+ borderRadius: 9999,
+ },
+ statusIndicatorGreen: {
+ backgroundColor: theme.colors.success,
+ },
+ statusIndicatorRed: {
+ backgroundColor: theme.colors.destructive,
+ },
+ statusText: {
+ color: theme?.colors?.typography,
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/app/+html.tsx b/apps/cli/templates/frontend/native/unistyles/app/+html.tsx
new file mode 100644
index 0000000..c495253
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/+html.tsx
@@ -0,0 +1,48 @@
+import { ScrollViewStyleReset } from 'expo-router/html';
+
+import '../unistyles';
+
+// This file is web-only and used to configure the root HTML for every
+// web page during static rendering.
+// The contents of this function only run in Node.js environments and
+// do not have access to the DOM or browser APIs.
+export default function Root({ children }: { children: React.ReactNode }) {
+ return (
+
+
+
+
+
+ {/*
+ This viewport disables scaling which makes the mobile website act more like a native app.
+ However this does reduce built-in accessibility. If you want to enable scaling, use this instead:
+
+ */}
+
+ {/*
+ Disable body scrolling on web. This makes ScrollView components work closer to how they do on native.
+ However, body scrolling is often nice to have for mobile web. If you want to enable it, remove this line.
+ */}
+
+
+ {/* Using raw CSS styles as an escape-hatch to ensure the background color never flickers in dark-mode. */}
+
+ {/* Add any additional elements that you want globally available on web... */}
+
+ {children}
+
+ );
+}
+
+const responsiveBackground = `
+body {
+ background-color: #fff;
+}
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #000;
+ }
+}`;
diff --git a/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx b/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx
new file mode 100644
index 0000000..9d37843
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/+not-found.tsx
@@ -0,0 +1,34 @@
+import { Link, Stack } from "expo-router";
+import { Text } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+import { Container } from "@/components/container";
+
+export default function NotFoundScreen() {
+ return (
+ <>
+
+
+ This screen doesn't exist.
+
+ Go to home screen!
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ title: {
+ fontSize: 20,
+ fontWeight: "bold",
+ color: theme.colors.typography,
+ },
+ link: {
+ marginTop: 16,
+ paddingVertical: 16,
+ },
+ linkText: {
+ fontSize: 14,
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs b/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs
new file mode 100644
index 0000000..52d6a0d
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/_layout.tsx.hbs
@@ -0,0 +1,77 @@
+{{#if (eq api "trpc")}}
+import { queryClient } from "@/utils/trpc";
+{{/if}}
+{{#if (eq api "orpc")}}
+import { queryClient } from "@/utils/orpc";
+{{/if}}
+{{#if (eq backend "convex")}}
+import { ConvexProvider, ConvexReactClient } from "convex/react";
+{{else}}
+import { QueryClientProvider } from "@tanstack/react-query";
+{{/if}}
+import { Stack } from "expo-router";
+import { GestureHandlerRootView } from "react-native-gesture-handler";
+import { useUnistyles } from "react-native-unistyles";
+
+export const unstable_settings = {
+ // Ensure that reloading on `/modal` keeps a back button present.
+ initialRouteName: "(drawer)",
+};
+
+{{#if (eq backend "convex")}}
+const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!, {
+ unsavedChangesWarning: false,
+});
+{{/if}}
+
+export default function RootLayout() {
+ const { theme } = useUnistyles();
+
+ return (
+ {{#if (eq backend "convex")}}
+
+
+
+
+
+
+
+
+ {{else}}
+
+
+
+
+
+
+
+
+ {{/if}}
+ );
+}
diff --git a/apps/cli/templates/frontend/native/unistyles/app/modal.tsx b/apps/cli/templates/frontend/native/unistyles/app/modal.tsx
new file mode 100644
index 0000000..1259346
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/app/modal.tsx
@@ -0,0 +1,29 @@
+import { Container } from "@/components/container";
+import { StatusBar } from "expo-status-bar";
+import { Platform, Text, View } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+export default function Modal() {
+ return (
+ <>
+
+
+
+ Model
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create((theme) => ({
+ text: {
+ color: theme.colors.typography,
+ },
+ container: {
+ flex: 1,
+ paddingBottom: 100,
+ justifyContent: "center",
+ alignItems: "center",
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/babel.config.js b/apps/cli/templates/frontend/native/unistyles/babel.config.js
new file mode 100644
index 0000000..43ccd03
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/babel.config.js
@@ -0,0 +1,20 @@
+module.exports = function (api) {
+ api.cache(true);
+ const plugins = [];
+
+ plugins.push([
+ 'react-native-unistyles/plugin',
+ {
+ autoProcessRoot: 'app',
+ autoProcessImports: ['@/components'],
+ },
+ ]);
+
+ plugins.push('react-native-reanimated/plugin');
+
+ return {
+ presets: ['babel-preset-expo'],
+
+ plugins,
+ };
+};
diff --git a/apps/cli/templates/frontend/native/unistyles/breakpoints.ts b/apps/cli/templates/frontend/native/unistyles/breakpoints.ts
new file mode 100644
index 0000000..249192f
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/breakpoints.ts
@@ -0,0 +1,9 @@
+export const breakpoints = {
+ xs: 0,
+ sm: 576,
+ md: 768,
+ lg: 992,
+ xl: 1200,
+ superLarge: 2000,
+ tvLike: 4000,
+} as const;
diff --git a/apps/cli/templates/frontend/native/unistyles/components/container.tsx b/apps/cli/templates/frontend/native/unistyles/components/container.tsx
new file mode 100644
index 0000000..44a18b3
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/components/container.tsx
@@ -0,0 +1,20 @@
+import React from "react";
+import { View } from "react-native";
+import { StyleSheet } from "react-native-unistyles";
+
+export const Container = ({ children }: { children: React.ReactNode }) => {
+ return {children};
+};
+
+const styles = StyleSheet.create((theme, rt) => ({
+ container: {
+ flex: 1,
+ paddingBottom: rt.insets.bottom,
+ backgroundColor: theme.colors.background,
+ transform: [
+ {
+ translateY: rt.insets.ime * -1,
+ },
+ ],
+ },
+}));
diff --git a/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx b/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx
new file mode 100644
index 0000000..c998f53
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/components/header-button.tsx
@@ -0,0 +1,31 @@
+import FontAwesome from '@expo/vector-icons/FontAwesome';
+import { forwardRef } from 'react';
+import { Pressable, StyleSheet } from 'react-native';
+
+export const HeaderButton = forwardRef void }>(
+ ({ onPress }, ref) => {
+ return (
+
+ {({ pressed }) => (
+
+ )}
+
+ );
+ }
+);
+
+export const styles = StyleSheet.create({
+ headerRight: {
+ marginRight: 15,
+ },
+});
diff --git a/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx b/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx
new file mode 100644
index 0000000..e75c9d3
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/components/tabbar-icon.tsx
@@ -0,0 +1,15 @@
+import FontAwesome from '@expo/vector-icons/FontAwesome';
+import { StyleSheet } from 'react-native';
+
+export const TabBarIcon = (props: {
+ name: React.ComponentProps['name'];
+ color: string;
+}) => {
+ return ;
+};
+
+export const styles = StyleSheet.create({
+ tabBarIcon: {
+ marginBottom: -3,
+ },
+});
diff --git a/apps/cli/templates/frontend/native/unistyles/expo-env.d.ts b/apps/cli/templates/frontend/native/unistyles/expo-env.d.ts
new file mode 100644
index 0000000..5411fdd
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/expo-env.d.ts
@@ -0,0 +1,3 @@
+///
+
+// NOTE: This file should not be edited and should be in your git ignore
\ No newline at end of file
diff --git a/apps/cli/templates/frontend/native/unistyles/index.js b/apps/cli/templates/frontend/native/unistyles/index.js
new file mode 100644
index 0000000..1df3241
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/index.js
@@ -0,0 +1,2 @@
+import 'expo-router/entry';
+import './unistyles';
diff --git a/apps/cli/templates/frontend/native/unistyles/metro.config.js b/apps/cli/templates/frontend/native/unistyles/metro.config.js
new file mode 100644
index 0000000..ab46948
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/metro.config.js
@@ -0,0 +1,20 @@
+// Learn more https://docs.expo.io/guides/customizing-metro
+const { getDefaultConfig } = require("expo/metro-config");
+const path = require("path");
+
+const workspaceRoot = path.resolve(__dirname, "../..");
+const projectRoot = __dirname;
+
+const config = getDefaultConfig(projectRoot);
+
+// 1. Watch all files within the monorepo
+config.watchFolders = [workspaceRoot];
+// 2. Let Metro know where to resolve packages, and in what order
+config.resolver.nodeModulesPaths = [
+ path.resolve(projectRoot, "node_modules"),
+ path.resolve(workspaceRoot, "node_modules"),
+];
+// 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths`
+config.resolver.disableHierarchicalLookup = true;
+
+module.exports = config;
diff --git a/apps/cli/templates/frontend/native/unistyles/package.json b/apps/cli/templates/frontend/native/unistyles/package.json
new file mode 100644
index 0000000..988b8cc
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/package.json
@@ -0,0 +1,47 @@
+{
+ "name": "native",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "dev": "expo start --clear",
+ "android": "expo run:android",
+ "ios": "expo run:ios",
+ "web": "expo start --web"
+ },
+ "dependencies": {
+ "@better-auth/expo": "^1.2.7",
+ "@expo/vector-icons": "^14.0.0",
+ "@react-navigation/bottom-tabs": "^7.0.5",
+ "@react-navigation/drawer": "^7.0.0",
+ "@react-navigation/native": "^7.0.3",
+ "@tanstack/react-form": "^1.0.5",
+ "babel-plugin-react-compiler": "^19.0.0-beta-af1b7da-20250417",
+ "expo": "^53.0.8",
+ "expo-constants": "~17.1.4",
+ "expo-linking": "~7.1.4",
+ "expo-router": "~5.0.3",
+ "expo-secure-store": "~14.2.3",
+ "expo-status-bar": "~2.2.3",
+ "expo-system-ui": "~5.0.6",
+ "expo-dev-client": "~5.1.8",
+ "expo-web-browser": "~14.1.6",
+ "react": "19.0.0",
+ "react-dom": "19.0.0",
+ "react-native": "0.79.2",
+ "react-native-edge-to-edge": "1.6.0",
+ "react-native-gesture-handler": "~2.24.0",
+ "react-native-nitro-modules": "0.25.2",
+ "react-native-reanimated": "~3.17.4",
+ "react-native-safe-area-context": "5.4.0",
+ "react-native-screens": "~4.10.0",
+ "react-native-unistyles": "3.0.0-rc.3",
+ "react-native-web": "^0.20.0"
+ },
+ "devDependencies": {
+ "ajv": "^8.12.0",
+ "@babel/core": "^7.20.0",
+ "@types/react": "~19.0.10",
+ "typescript": "~5.8.3"
+ },
+ "private": true
+}
diff --git a/apps/cli/templates/frontend/native/unistyles/theme.ts b/apps/cli/templates/frontend/native/unistyles/theme.ts
new file mode 100644
index 0000000..12694bf
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/theme.ts
@@ -0,0 +1,35 @@
+const sharedColors = {
+ success: "#22C55E",
+ destructive: "#DC2626",
+ border: "#D1D5DB",
+} as const;
+
+export const lightTheme = {
+ colors: {
+ ...sharedColors,
+ typography: "#000000",
+ background: "#ffffff",
+ primary: "#3B82F6",
+ },
+ margins: {
+ sm: 2,
+ md: 4,
+ lg: 8,
+ xl: 12,
+ },
+} as const;
+
+export const darkTheme = {
+ colors: {
+ ...sharedColors,
+ typography: "#ffffff",
+ background: "#000000",
+ primary: "#60A5FA",
+ },
+ margins: {
+ sm: 2,
+ md: 4,
+ lg: 8,
+ xl: 12,
+ },
+} as const;
diff --git a/apps/cli/templates/frontend/native/unistyles/tsconfig.json b/apps/cli/templates/frontend/native/unistyles/tsconfig.json
new file mode 100644
index 0000000..9b197f6
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "expo/tsconfig.base",
+ "compilerOptions": {
+ "strict": true,
+ "jsx": "react-jsx",
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["*"]
+ }
+ },
+ "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
+}
diff --git a/apps/cli/templates/frontend/native/unistyles/unistyles.ts b/apps/cli/templates/frontend/native/unistyles/unistyles.ts
new file mode 100644
index 0000000..888a700
--- /dev/null
+++ b/apps/cli/templates/frontend/native/unistyles/unistyles.ts
@@ -0,0 +1,27 @@
+import { StyleSheet } from 'react-native-unistyles';
+
+import { breakpoints } from './breakpoints';
+import { lightTheme, darkTheme } from './theme';
+
+type AppBreakpoints = typeof breakpoints;
+
+type AppThemes = {
+ light: typeof lightTheme;
+ dark: typeof darkTheme;
+};
+
+declare module 'react-native-unistyles' {
+ export interface UnistylesBreakpoints extends AppBreakpoints {}
+ export interface UnistylesThemes extends AppThemes {}
+}
+
+StyleSheet.configure({
+ breakpoints,
+ themes: {
+ light: lightTheme,
+ dark: darkTheme,
+ },
+ settings: {
+ adaptiveThemes: true,
+ },
+});
diff --git a/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs b/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs
index 7da3ad5..bbc2830 100644
--- a/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs
+++ b/apps/cli/templates/frontend/react/web-base/src/components/header.tsx.hbs
@@ -41,10 +41,10 @@ export default function Header() {
);
{{else if (includes frontend "react-router")}}
return (
- isActive ? "font-bold" : ""}
+ isActive ? "font-bold" : ""}
end
>
{label}
@@ -60,7 +60,6 @@ export default function Header() {
);
{{else}}
- // Fallback case (shouldn't happen with valid frontend selection)
return null;
{{/if}}
})}
@@ -77,4 +76,4 @@ export default function Header() {
);
-}
\ No newline at end of file
+}
diff --git a/apps/web/src/app/(home)/_components/StackArchitech.tsx b/apps/web/src/app/(home)/_components/StackArchitech.tsx
index 0fd421e..54a42a9 100644
--- a/apps/web/src/app/(home)/_components/StackArchitech.tsx
+++ b/apps/web/src/app/(home)/_components/StackArchitech.tsx
@@ -92,7 +92,9 @@ const hasWebFrontend = (frontend: string[]) =>
].includes(f),
);
-const hasNativeFrontend = (frontend: string[]) => frontend.includes("native");
+const checkHasNativeFrontend = (frontend: string[]) =>
+ frontend.includes("native-nativewind") ||
+ frontend.includes("native-unistyles");
const hasPWACompatibleFrontend = (frontend: string[]) =>
frontend.some((f) =>
@@ -183,6 +185,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
}
const isConvex = nextStack.backend === "convex";
+ const isBackendNone = nextStack.backend === "none";
if (isConvex) {
const convexOverrides: Partial = {
@@ -247,6 +250,40 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
message: "Frontend defaulted to TanStack Router",
});
}
+ } else if (isBackendNone) {
+ const noneOverrides: Partial = {
+ auth: "false",
+ database: "none",
+ orm: "none",
+ api: "none",
+ runtime: "none",
+ dbSetup: "none",
+ examples: [],
+ };
+
+ for (const [key, value] of Object.entries(noneOverrides)) {
+ const catKey = key as keyof StackState;
+ if (JSON.stringify(nextStack[catKey]) !== JSON.stringify(value)) {
+ const displayName = getCategoryDisplayName(catKey);
+ const valueDisplay = Array.isArray(value) ? "none" : value;
+ const message = `${displayName} set to '${valueDisplay}'`;
+
+ notes[catKey].notes.push(
+ `No backend selected: ${displayName} will be set to '${valueDisplay}'.`,
+ );
+ notes.backend.notes.push(
+ `No backend requires ${displayName} to be '${valueDisplay}'.`,
+ );
+ notes[catKey].hasIssue = true;
+ notes.backend.hasIssue = true;
+ (nextStack[catKey] as string | string[]) = value;
+ changed = true;
+ changes.push({
+ category: "backend-none",
+ message,
+ });
+ }
+ }
} else {
if (nextStack.runtime === "none") {
notes.runtime.notes.push(
@@ -562,7 +599,7 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const incompatibleExamples: string[] = [];
const isWeb = hasWebFrontend(nextStack.frontend);
- const isNativeOnly = hasNativeFrontend(nextStack.frontend) && !isWeb;
+ const isNativeOnly = checkHasNativeFrontend(nextStack.frontend) && !isWeb;
if (isNativeOnly) {
if (nextStack.examples.length > 0) {
@@ -694,17 +731,19 @@ const analyzeStackCompatibility = (stack: StackState): CompatibilityResult => {
const getCompatibilityRules = (stack: StackState) => {
const isConvex = stack.backend === "convex";
+ const isBackendNone = stack.backend === "none";
const hasWebFrontendSelected = hasWebFrontend(stack.frontend);
- const hasNativeOnly =
- hasNativeFrontend(stack.frontend) && !hasWebFrontendSelected;
+ const hasNativeFrontend = checkHasNativeFrontend(stack.frontend);
+ const hasNativeOnly = hasNativeFrontend && !hasWebFrontendSelected;
const hasSolid = stack.frontend.includes("solid");
const hasNuxt = stack.frontend.includes("nuxt");
const hasSvelte = stack.frontend.includes("svelte");
return {
isConvex,
+ isBackendNone,
hasWebFrontend: hasWebFrontendSelected,
- hasNativeFrontend: hasNativeFrontend(stack.frontend),
+ hasNativeFrontend,
hasNativeOnly,
hasPWACompatible: hasPWACompatibleFrontend(stack.frontend),
hasTauriCompatible: hasTauriCompatibleFrontend(stack.frontend),
@@ -853,7 +892,7 @@ const StackArchitect = () => {
};
for (const category of CATEGORY_ORDER) {
- const options = TECH_OPTIONS[category] || [];
+ const options = TECH_OPTIONS[category as keyof typeof TECH_OPTIONS] || [];
const catKey = category as keyof StackState;
for (const tech of options) {
@@ -879,7 +918,9 @@ const StackArchitect = () => {
addRule(
category,
techId,
- `Convex backend requires ${getCategoryDisplayName(catKey)} to be '${requiredValue}'.`,
+ `Convex backend requires ${getCategoryDisplayName(
+ catKey,
+ )} to be '${requiredValue}'.`,
);
}
}
@@ -897,12 +938,43 @@ const StackArchitect = () => {
addRule(
category,
techId,
- `${tech.name} is not compatible with Convex backend.`,
+ `Convex backend is not compatible with ${tech.name}.`,
);
}
continue;
}
+ if (rules.isBackendNone) {
+ if (
+ [
+ "auth",
+ "database",
+ "orm",
+ "api",
+ "runtime",
+ "dbSetup",
+ "examples",
+ ].includes(catKey)
+ ) {
+ if (
+ (catKey === "auth" && techId === "true") ||
+ ((catKey === "database" ||
+ catKey === "orm" ||
+ catKey === "api" ||
+ catKey === "runtime" ||
+ catKey === "dbSetup") &&
+ techId !== "none") ||
+ (catKey === "examples" && techId !== "none")
+ ) {
+ addRule(
+ category,
+ techId,
+ `Cannot be selected when 'No Backend' is chosen. Will be set to 'None' or disabled.`,
+ );
+ }
+ }
+ }
+
if (catKey === "runtime" && techId === "none") {
addRule(
category,
@@ -934,114 +1006,68 @@ const StackArchitect = () => {
}
if (catKey === "orm") {
- if (stack.database === "none" && techId !== "none") {
- addRule(
- category,
- techId,
- "Select a database to enable ORM options.",
- );
- }
- if (
- stack.database === "mongodb" &&
- techId !== "prisma" &&
- techId !== "mongoose" &&
- techId !== "none"
- ) {
- addRule(
- category,
- techId,
- "MongoDB requires the Prisma or Mongoose ORM.",
- );
- }
- if (
- stack.database !== "mongodb" &&
- stack.database !== "none" &&
- techId === "mongoose"
- ) {
- addRule(
- category,
- techId,
- "Mongoose ORM is only compatible with MongoDB.",
- );
- }
- if (stack.dbSetup === "turso" && techId !== "drizzle") {
- addRule(category, techId, "Turso DB setup requires Drizzle ORM.");
- }
- if (stack.dbSetup === "prisma-postgres" && techId !== "prisma") {
- addRule(
- category,
- techId,
- "Prisma PostgreSQL setup requires Prisma ORM.",
- );
- }
- if (
- stack.dbSetup === "mongodb-atlas" &&
- techId !== "prisma" &&
- techId !== "mongoose"
- ) {
- addRule(
- category,
- techId,
- "MongoDB Atlas setup requires Prisma or Mongoose ORM.",
- );
- }
if (techId === "none") {
- if (stack.database === "mongodb") {
- addRule(
- category,
- techId,
- "MongoDB requires Prisma or Mongoose ORM.",
- );
- }
- if (stack.dbSetup === "turso") {
- addRule(category, techId, "Turso DB setup requires Drizzle ORM.");
- }
- if (stack.dbSetup === "prisma-postgres") {
- addRule(category, techId, "This DB setup requires Prisma ORM.");
- }
- if (stack.dbSetup === "mongodb-atlas") {
- addRule(
- category,
- techId,
- "This DB setup requires Prisma or Mongoose ORM.",
- );
- }
+ addRule(
+ category,
+ techId,
+ "ORM 'None' is only available with the Convex backend.",
+ );
}
}
if (catKey === "dbSetup" && techId !== "none") {
- if (stack.database === "none") {
- addRule(
- category,
- techId,
- "Select a database before choosing a cloud setup.",
- );
- } else {
- if (techId === "turso") {
- if (stack.database !== "sqlite") {
- addRule(category, techId, "Turso requires SQLite database.");
- }
- if (stack.orm !== "drizzle") {
- addRule(category, techId, "Turso requires Drizzle ORM.");
- }
- } else if (techId === "prisma-postgres") {
- if (stack.database !== "postgres") {
- addRule(category, techId, "Requires PostgreSQL database.");
- }
- if (stack.orm !== "prisma") {
- addRule(category, techId, "Requires Prisma ORM.");
- }
- } else if (techId === "mongodb-atlas") {
- if (stack.database !== "mongodb") {
- addRule(category, techId, "Requires MongoDB database.");
- }
- if (stack.orm !== "prisma" && stack.orm !== "mongoose") {
- addRule(category, techId, "Requires Prisma or Mongoose ORM.");
- }
- } else if (techId === "neon") {
- if (stack.database !== "postgres") {
- addRule(category, techId, "Requires PostgreSQL database.");
- }
+ if (techId === "turso") {
+ if (stack.database !== "sqlite") {
+ addRule(
+ category,
+ techId,
+ "Turso requires SQLite. It will be selected.",
+ );
+ }
+ if (stack.orm !== "drizzle") {
+ addRule(
+ category,
+ techId,
+ "Turso requires Drizzle ORM. It will be selected.",
+ );
+ }
+ } else if (techId === "prisma-postgres") {
+ if (stack.database !== "postgres") {
+ addRule(
+ category,
+ techId,
+ "Prisma PostgreSQL setup requires PostgreSQL. It will be selected.",
+ );
+ }
+ if (stack.orm !== "prisma") {
+ addRule(
+ category,
+ techId,
+ "Prisma PostgreSQL setup requires Prisma ORM. It will be selected.",
+ );
+ }
+ } else if (techId === "mongodb-atlas") {
+ if (stack.database !== "mongodb") {
+ addRule(
+ category,
+ techId,
+ "MongoDB Atlas setup requires MongoDB. It will be selected.",
+ );
+ }
+ if (stack.orm !== "prisma" && stack.orm !== "mongoose") {
+ addRule(
+ category,
+ techId,
+ "MongoDB Atlas setup requires Prisma or Mongoose ORM. Prisma will be selected.",
+ );
+ }
+ } else if (techId === "neon") {
+ if (stack.database !== "postgres") {
+ addRule(
+ category,
+ techId,
+ "Neon requires PostgreSQL. It will be selected.",
+ );
}
}
}
@@ -1055,56 +1081,164 @@ const StackArchitect = () => {
}
if (catKey === "addons") {
- if (techId === "pwa" && !rules.hasPWACompatible) {
+ const incompatibleAddons: string[] = [];
+ const isPWACompat = hasPWACompatibleFrontend(stack.frontend);
+ const isTauriCompat = hasTauriCompatibleFrontend(stack.frontend);
+
+ if (!isPWACompat && stack.addons.includes("pwa")) {
+ incompatibleAddons.push("pwa");
addRule(
category,
techId,
- "Requires TanStack Router, React Router or Solid frontend.",
+ "PWA addon removed (requires compatible frontend)",
);
}
- if (techId === "tauri" && !rules.hasTauriCompatible) {
+ if (!isTauriCompat && stack.addons.includes("tauri")) {
+ incompatibleAddons.push("tauri");
addRule(
category,
techId,
- "Requires TanStack Router, React Router, Nuxt, Svelte or Solid frontend.",
+ "Tauri addon removed (requires compatible frontend)",
+ );
+ }
+
+ const originalAddonsLength = stack.addons.length;
+ if (incompatibleAddons.length > 0) {
+ stack.addons = stack.addons.filter(
+ (addon) => !incompatibleAddons.includes(addon),
+ );
+ if (stack.addons.length !== originalAddonsLength) {
+ addRule(
+ category,
+ techId,
+ "Addons filtered (requires compatible frontend)",
+ );
+ }
+ }
+
+ if (
+ stack.addons.includes("husky") &&
+ !stack.addons.includes("biome")
+ ) {
+ addRule(
+ category,
+ techId,
+ "Husky addon is selected without Biome. Consider adding Biome for lint-staged integration.",
);
}
}
if (catKey === "examples") {
- if (rules.hasNativeOnly) {
- addRule(
- category,
- techId,
- "Examples are not supported with Native-only frontend.",
- );
- } else {
- if (!rules.hasWebFrontend) {
+ const incompatibleExamples: string[] = [];
+ const isWeb = hasWebFrontend(stack.frontend);
+ const isNativeOnly = checkHasNativeFrontend(stack.frontend) && !isWeb;
+
+ if (isNativeOnly) {
+ if (stack.examples.length > 0) {
addRule(
category,
techId,
- "Requires a web frontend (TanStack Router, React Router, etc.).",
+ "Examples removed (not supported with Native-only frontend)",
);
}
- if (techId === "todo" && stack.database === "none") {
- addRule(category, techId, "Todo example requires a database.");
+ } else {
+ if (!isWeb) {
+ if (stack.examples.includes("todo")) {
+ incompatibleExamples.push("todo");
+ addRule(
+ category,
+ techId,
+ "Todo example removed (requires web frontend)",
+ );
+ }
+ if (stack.examples.includes("ai")) {
+ incompatibleExamples.push("ai");
+ addRule(
+ category,
+ techId,
+ "AI example removed (requires web frontend)",
+ );
+ }
}
- if (techId === "ai") {
- if (stack.backend === "elysia") {
+ if (stack.database === "none" && stack.examples.includes("todo")) {
+ incompatibleExamples.push("todo");
+ addRule(
+ category,
+ techId,
+ "Todo example removed (requires a database)",
+ );
+ }
+ if (stack.backend === "elysia" && stack.examples.includes("ai")) {
+ incompatibleExamples.push("ai");
+ addRule(
+ category,
+ techId,
+ "AI example removed (not compatible with Elysia)",
+ );
+ }
+ if (rules.hasSolid && stack.examples.includes("ai")) {
+ incompatibleExamples.push("ai");
+ addRule(
+ category,
+ techId,
+ "AI example removed (not compatible with Solid)",
+ );
+ }
+ }
+
+ const uniqueIncompatibleExamples = [...new Set(incompatibleExamples)];
+ if (uniqueIncompatibleExamples.length > 0) {
+ if (!isWeb && !isNativeOnly) {
+ if (
+ uniqueIncompatibleExamples.includes("todo") ||
+ uniqueIncompatibleExamples.includes("ai")
+ ) {
addRule(
category,
techId,
- "AI example is not compatible with Elysia backend.",
- );
- }
- if (rules.hasSolid) {
- addRule(
- category,
- techId,
- "AI example is not compatible with Solid frontend.",
+ "Examples require a web frontend. Incompatible examples will be removed.",
);
}
}
+ if (
+ stack.database === "none" &&
+ uniqueIncompatibleExamples.includes("todo")
+ ) {
+ addRule(
+ category,
+ techId,
+ "Todo example requires a database. It will be removed.",
+ );
+ }
+ if (
+ stack.backend === "elysia" &&
+ uniqueIncompatibleExamples.includes("ai")
+ ) {
+ addRule(
+ category,
+ techId,
+ "AI example is not compatible with Elysia. It will be removed.",
+ );
+ }
+ if (rules.hasSolid && uniqueIncompatibleExamples.includes("ai")) {
+ addRule(
+ category,
+ techId,
+ "AI example is not compatible with Solid. It will be removed.",
+ );
+ }
+
+ const originalExamplesLength = stack.examples.length;
+ stack.examples = stack.examples.filter(
+ (ex) => !uniqueIncompatibleExamples.includes(ex),
+ );
+ if (stack.examples.length !== originalExamplesLength) {
+ addRule(
+ category,
+ techId,
+ "Examples filtered (incompatible examples removed)",
+ );
+ }
}
}
}
@@ -1256,6 +1390,8 @@ const StackArchitect = () => {
nextArray = nextArray.filter((id) => id !== "none");
if (webTypes.includes(techId)) {
nextArray = nextArray.filter((id) => !webTypes.includes(id));
+ } else if (techId.startsWith("native-")) {
+ nextArray = nextArray.filter((id) => !id.startsWith("native-"));
}
nextArray.push(techId);
}
diff --git a/apps/web/src/lib/constant.ts b/apps/web/src/lib/constant.ts
index 89037b2..95cd058 100644
--- a/apps/web/src/lib/constant.ts
+++ b/apps/web/src/lib/constant.ts
@@ -75,13 +75,21 @@ export const TECH_OPTIONS = {
default: false,
},
{
- id: "native",
- name: "React Native",
- description: "Expo with NativeWind",
+ id: "native-nativewind",
+ name: "React Native + NativeWind",
+ description: "Expo with NativeWind (Tailwind)",
icon: "/icon/expo.svg",
color: "from-purple-400 to-purple-600",
default: false,
},
+ {
+ id: "native-unistyles",
+ name: "React Native + Unistyles",
+ description: "Expo with Unistyles",
+ icon: "/icon/expo.svg",
+ color: "from-pink-400 to-pink-600",
+ default: false,
+ },
{
id: "none",
name: "No Frontend",
@@ -145,6 +153,13 @@ export const TECH_OPTIONS = {
icon: "/icon/convex.svg",
color: "from-pink-500 to-pink-700",
},
+ {
+ id: "none",
+ name: "No Backend",
+ description: "Skip backend integration (frontend only)",
+ icon: "⚙️",
+ color: "from-gray-400 to-gray-600",
+ },
],
database: [
{
@@ -447,7 +462,7 @@ export const PRESET_TEMPLATES = [
description: "React Native with Expo and SQLite database",
stack: {
projectName: "my-better-t-app",
- frontend: ["native"],
+ frontend: ["native-nativewind"],
runtime: "bun",
backend: "hono",
database: "sqlite",
@@ -489,7 +504,7 @@ export const PRESET_TEMPLATES = [
description: "Complete setup with web, native, Turso, and addons",
stack: {
projectName: "my-better-t-app",
- frontend: ["tanstack-router", "native"],
+ frontend: ["tanstack-router", "native-nativewind"],
runtime: "bun",
backend: "hono",
database: "sqlite",