create-vex-app
v1.1.0
Published
A monorepo template with Vite React frontend, HonoJS backend, and shared lib package
Downloads
276
Maintainers
Readme
Vex App Template
Stop wrestling with annoying AI slop. Vex App is a Bun-powered full-stack starter kit with Vite + React, Hono, and a shared TypeScript lib, built with strict guardrails that make AI agents write production-quality code from the first keystroke.
🚀 Quickstart
Create a new app in the current directory (you'll be prompted for a name):
bun create vex-app@latestOr specify the name directly:
bun create vex-app@latest my-appThe generator will:
- Copy the template into
./my-app(or the name you provide) - Rename all package scopes to
@<your-app>/... - Seed env files with sensible defaults
- Optionally include GitHub CI/CD pipeline
- Optionally configure AI settings (strict ESLint, Cursor rules, post-write hooks)
- Optionally enable the spec-first workflow (AI writes test specs before implementing)
- Ask to run
bun install(workspaces) and optionally buildlib
📁 Project Structure
my-app/
├── frontend/ # React + Vite application
│ ├── src/
│ │ ├── routes/ # File-based routes (TanStack Router)
│ │ │ ├── __root.tsx # Root layout (providers, shared UI)
│ │ │ └── index.tsx # Home page (/)
│ │ ├── api/ # API call utilities
│ │ ├── components/ # Reusable UI components
│ │ └── env/ # Environment variable validation
│ │ ├── schema.ts # Zod schema (edit this to add vars)
│ │ ├── validate.ts # Validation logic
│ │ └── env.ts # Typed env object
│ └── vite.config.ts # Vite config (plugins, proxy, aliases)
├── backend/ # Hono API server
│ ├── src/
│ │ ├── index.ts # Server entry (CORS, CSRF, auth, routes)
│ │ ├── db/schema/ # Drizzle ORM table definitions
│ │ ├── env/ # Environment variable validation
│ │ └── security/ # Rate limiting, token encryption, hashing
│ └── drizzle.config.ts # Drizzle Kit config
├── lib/ # Shared TypeScript utilities and types
│ └── src/
│ ├── utils/ # assertNever, raise, tryCatch, objectUtils
│ └── types/ # Shared types (User, etc.)
├── scripts/ # Dev script (parallel service runner)
├── .cursor/rules/ # Cursor AI rules for project conventions
├── .cursor/hooks/ # Pre-write and post-write hooks (ESLint, Prettier, tsc, jscpd, spec enforcement)
├── .spec-pending # Spec-first workflow lock file (git-ignored, only if spec-first enabled)
├── .jscpd.json # Duplicate code detection config
├── .github/workflows/ # CI/CD pipeline
├── package.json # Root workspace config & scripts
├── tsconfig.json # TypeScript project references
├── .prettierrc # Prettier config
└── .prettierignore # Files excluded from Prettier💻 Development Commands (run from repo root)
| Command | Description |
| ------------------------ | ------------------------------------------------ |
| bun run dev | Start frontend, lib, and backend |
| bun run dev:frontend | Start only the frontend (Vite, port 5173) |
| bun run dev:backend | Start only the backend (Hono, port 3000) |
| bun run dev:lib | Watch mode for lib package (rebuilds on changes) |
| bun run build | Build all packages (includes typecheck & lint) |
| bun run build:all | Build all packages without typecheck/lint |
| bun run build:frontend | Build only the frontend package |
| bun run build:backend | Build only the backend package |
| bun run build:lib | Build only the lib package |
| bun run typecheck | Type check all packages |
| bun run test | Run all tests across all packages |
| bun run test:frontend | Run tests in the frontend package only |
| bun run test:backend | Run tests in the backend package only |
| bun run test:lib | Run tests in the lib package only |
| bun run lint | Lint all packages (format check + ESLint) |
| bun run lint:fix | Auto-fix linting issues |
| bun run format | Format all files with Prettier |
| bun run format:check | Check formatting without fixing |
Database Commands
| Command | Description |
| --------------------- | ------------------------------- |
| bun run db:generate | Generate database migrations |
| bun run db:migrate | Run database migrations |
| bun run db:push | Push schema changes to database |
| bun run db:studio | Open Drizzle Studio |
📦 Using the Shared Library Package
The lib package contains shared TypeScript utilities that can be imported in both the frontend and backend:
import { raise, tryCatch, tryCatchAsync, assertNever } from "@your-project/lib";
import type { User } from "@your-project/lib";Important: If you make changes to the lib package, you must rebuild it:
bun run build:libOr run it in watch mode during development (already included in bun run dev):
bun run dev:lib🗄️ Database Setup
This template uses Drizzle ORM with PostgreSQL. The database schema is defined in backend/src/db/schema/.
Setup Steps
Set up your database (local PostgreSQL or cloud provider)
Configure environment variables in
backend/.env(see Environment Variables section)Create your schema in
backend/src/db/schema/(e.g.,users.ts,posts.ts)Push schema to database:
bun run db:pushOr generate and run migrations:
bun run db:generate bun run db:migrateOpen Drizzle Studio to view/edit data:
bun run db:studio
🔐 Authentication Setup
This template includes Auth.js (formerly NextAuth) integration with Google OAuth as the default provider. Authentication uses database sessions for secure, server-side session management.
Backend Setup
Set up Google OAuth credentials:
- Go to Google Cloud Console
- Create a new OAuth 2.0 Client ID (Web application)
- Add authorized redirect URI:
http://localhost:3000/api/auth/callback/google - Copy the Client ID and Client Secret
Configure backend environment variables in
backend/.env:AUTH_SECRET=your-auth-secret-here # Generate with: openssl rand -base64 32 DATABASE_URL=postgresql://user:password@localhost:5432/dbname FRONTEND_URL=http://localhost:5173 GOOGLE_CLIENT_ID=your-google-client-id GOOGLE_CLIENT_SECRET=your-google-client-secret PORT=3000Push the auth schema to your database:
bun run db:push
Frontend Setup
Configure frontend environment variables in
frontend/.env:VITE_BACKEND_URL=http://localhost:3000 VITE_PORT=5173The frontend is already configured with
SessionProvider(in__root.tsx) and auth hooks. The sign-in button and session management are ready to use.Vite proxy is configured — Auth API requests (
/api/auth/*) are automatically proxied to the backend viavite.config.ts. This allows the frontend to use relative URLs for auth endpoints.
Adding More Providers
To add additional OAuth providers (GitHub, Discord, etc.):
Add provider to backend in
backend/src/index.ts:import GitHub from "@auth/core/providers/github"; providers: [ Google({ clientId: env.GOOGLE_CLIENT_ID, clientSecret: env.GOOGLE_CLIENT_SECRET }), GitHub({ clientId: env.GITHUB_CLIENT_ID, clientSecret: env.GITHUB_CLIENT_SECRET }), ],Add environment variables to
backend/src/env/env.ts(schema) andbackend/.env(values)Update the frontend sign-in button:
<button onClick={() => signIn("github")}>Sign in with GitHub</button>
🛠️ Tech Stack
Frontend
- React 19 - UI library
- Vite 5 - Build tool and dev server
- TypeScript 5.9 - Type safety
- TanStack Router - File-based routing with automatic code splitting
- TanStack Query (React Query) - Data fetching and caching
- Tailwind CSS v4 - Styling
- Zod v4 - Runtime type validation
- @hono/auth-js/react - Auth.js React client hooks
Backend
- Hono v4 - Fast web framework
- TypeScript 5.9 - Type safety
- Drizzle ORM - Type-safe SQL ORM
- PostgreSQL - Database (via Bun's built-in SQL driver)
- Zod v4 - Runtime type validation
- Auth.js (@hono/auth-js) - Authentication framework
- @auth/drizzle-adapter - Drizzle adapter for Auth.js
Shared
- Bun - Runtime and package manager
- ESLint - Code linting
- Prettier - Code formatting
🔒 Security Defaults
Out of the box, the backend enables several defenses:
- Secure headers via
hono/secure-headers - CSRF protection via
hono/csrf; the frontend sendsX-CSRF-Tokenon mutating requests - CORS restricted to
FRONTEND_URLwith credentials andX-CSRF-Tokenallowed - Rate limiting on
/api/auth/*(10 requests / 60s per IP+path) to mitigate brute force and callback abuse - Sessions and verification tokens are stored as SHA-256 hashes (no plaintext)
- OAuth tokens (access/refresh/id) can be encrypted at rest (AES-256-GCM) when you set
OAUTH_TOKEN_ENCRYPTION_KEY
Recommended hardening for production (left to end users):
- Add a Content Security Policy (CSP) with nonces for scripts
- Consider an Origin/Referer check for POST/PUT/PATCH/DELETE as defense-in-depth
- Ensure cookies are
Secure,HttpOnly, andSameSite=Lax/Strictbehind HTTPS - If deploying multiple instances, replace in-memory rate limiting with a shared store (e.g., Redis)
🔧 Environment Variables
Backend (backend/.env)
# Authentication
AUTH_SECRET= # Generate with: openssl rand -base64 32
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# Environment
ENVIRONMENT=development # development | production | testing
# Frontend URL (for CORS and auth redirects)
FRONTEND_URL=http://localhost:5173
# Google OAuth (required)
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Token encryption (optional — encrypts OAuth tokens at rest)
OAUTH_TOKEN_ENCRYPTION_KEY= # Generate with: openssl rand -base64 32
# Server
PORT=3000Required variables:
AUTH_SECRET- Secret for signing sessions (generate withopenssl rand -base64 32)DATABASE_URL- PostgreSQL connection stringGOOGLE_CLIENT_ID- Google OAuth client IDGOOGLE_CLIENT_SECRET- Google OAuth client secret
Optional variables:
ENVIRONMENT- Runtime environment (default:development)FRONTEND_URL- Frontend URL for redirects (default:http://localhost:5173)OAUTH_TOKEN_ENCRYPTION_KEY- Encrypts OAuth tokens at rest with AES-256-GCMPORT- Backend server port (default:3000)
Frontend (frontend/.env)
VITE_BACKEND_URL=http://localhost:3000
VITE_PORT=5173VITE_BACKEND_URL- Backend API URL (required)VITE_PORT- Frontend dev server port (default:5173)
Environment variables are validated at startup with Zod. The backend logs errors and exits; the frontend renders a friendly error page in the browser. Edit frontend/src/env/schema.ts to add new frontend variables.
Note: All VITE_ variables are embedded in the client bundle and publicly visible. Never put secrets in frontend env vars. Restart the dev server after changing .env files.
📝 Code Quality
- TypeScript - Strict mode enabled across all packages
- ESLint - Type-aware linting across workspaces (TS project aware):
- Prefer
??over||for defaulting - Flag impossible conditions (
@typescript-eslint/no-unnecessary-condition) - Enforce
import typewhen symbols are used as types only - Alphabetical key sorting for cleaner diffs
- Prefer
- Prettier - Automatic code formatting (configured via
.prettierrc) - Type checking - Run
bun run typecheckto verify all packages
🤖 AI Integration
When you select "Use Vex App recommended AI settings" during setup, the CLI configures your project for AI-assisted development:
- Strict ESLint config — Swaps in a hardened ruleset with
sonarjs,unicorn, andperfectionistplugins. Enforces explicit return types, bans type assertions, prevents mutation, bans rawtry/catchblocks (usetryCatch/tryCatchAsyncutilities instead), limits complexity, and more. Designed to catch the mistakes AI models make most often. - Cursor rules (
.cursor/rules/) —.mdcfiles with frontmatter that Cursor automatically injects into the AI model's context. Cover component organization, Tailwind conventions, type safety patterns, Zod v4 usage, Bun APIs, backend architecture, and testing conventions. - Spec-first workflow (optional) — When enabled, the AI writes empty test specs (WHEN/AND/it decision trees) for every logical layer before writing any implementation code, then stops and asks you to approve the paths before building. Enforcement is mechanical — a pre-write hook blocks all implementation writes until you approve, using a git-ignored
.spec-pendingmarker file at the repo root. - Post-write hooks (
.cursor/hooks/) — Four shell scripts that run automatically after every AI file write:- Prettier — Auto-formats the file
- ESLint — Auto-fixes what it can, blocks the write if errors remain
- TypeScript — Runs
tsc --noEmit, blocks on type errors - jscpd — Detects duplicate code, blocks if clones are found
- Duplicate detection (
.jscpd.json) — Configures thresholds for copy-paste detection
If you opt out of AI settings, you get the standard ESLint config without extra plugins and no .cursor/ directory.
Note: The hooks require
jqto be installed on your system. Most macOS and Linux systems have it pre-installed.
🔧 Publishing This Template (For Maintainers)
To publish this template to npm so others can use it:
Make sure you're logged into npm:
npm loginBump the version and publish to npm:
npm version patch # or minor/major as appropriate npm publish --access public git push --follow-tagsUsers can then create projects with:
bun create vex-app@latest
Note: The package name is create-vex-app, but users call it with bun create vex-app (Bun automatically prepends create-).
