Everything you need to ship a full-stack single-page app and optional desktop build. A modern starter for React + Vite + Hono with shadcn/ui, designed for AI-assisted refactors and deploy-anywhere flexibility.
- Framework: React 19 with Vite 7
- Language: TypeScript 5.9
- Auth: Clerk (sign-in, sign-up, protected routes, JWT-secured API)
- UI Library: shadcn/ui (new-york style) + Lucide React icons
- Styling: Tailwind CSS v4
- Routing: React Router v7 (SPA)
- Backend: Hono (Node runtime; portable to Cloudflare Workers, Deno, Bun)
- Database: PostgreSQL with Drizzle ORM
- Desktop (optional): Tauri v2
- Monorepo: npm workspaces (
apps/web+apps/api)
- ✅ React 19 + Vite 7 — Fast SPA with HMR and simple build pipeline
- ✅ Hono API — Type-safe backend with RPC-style types flowing to the frontend
- ✅ Drizzle + PostgreSQL — Type-safe ORM with migrations and Drizzle Studio
- ✅ Tailwind CSS v4 — Utility-first styling with a single config
- ✅ shadcn/ui + Lucide — Button, theme variables,
cn()utility; add more withnpx shadcn@latest add <name> - ✅ Sidebar layout — Ready-made app shell with nav (Home, Settings) and main content area
- ✅ Typed API client — End-to-end types via Hono RPC; no codegen
- ✅ Deploy anywhere — Node runs on Railway, Render, Fly, and most hosts; same API can target Workers
- ✅ Clerk authentication — Sign-in, sign-up, and protected routes; API protected with JWT verification
- ✅ Optional desktop — Tauri v2 for a native shell around the same React app
- ✅ One repo, two apps — AI (and you) get a full view of frontend and backend; deploy together or separately
Before you begin, make sure you have the following installed:
- Node.js (version 18 or higher; 20+ recommended) — Download here
- npm (comes with Node.js)
- PostgreSQL (for the API database) — Download here or use a hosted service (Neon, Supabase, Railway, etc.)
- Rust (only if you use Tauri) — Install guide
To check your versions:
node --version # 18.0 or higher
npm --versiongit clone <your-repo-url> my-app
cd my-appnpm installCopy the example env file:
cp .env.example .envYou need a PostgreSQL database. Set one up locally or in the cloud with NeonDB or Supabase:
- Local Postgres: Install PostgreSQL, create a database, and set
DATABASE_URLin.env:# Example DATABASE_URL=postgresql://user:password@localhost:5432/your_db - NeonDB: If you have the Neon CLI installed, ask your AI agent to create a new database and save the connection string to
.env. Or create a project at neon.tech, copy the connection string from the dashboard, and paste it into.env. - Supabase: Create a project at supabase.com, go to Settings → Database, copy the connection string (URI format), and paste it into
.env.
Once DATABASE_URL is set, run migrations from the API package (or use db:push for prototyping):
npm run db:push --workspace=@launch-kit-spa-desktop-switchdimension/api
# Or: npm run db:migrate --workspace=@launch-kit-spa-desktop-switchdimension/apiThe app uses Clerk for authentication. Without keys the app shows a "Clerk not configured" screen with setup instructions.
-
Create a free account at clerk.com and create an application. When prompted to choose a framework, select React.
-
In the Clerk Dashboard, go to API Keys. The dashboard will show keys using Next.js naming conventions:
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_... CLERK_SECRET_KEY=sk_test_...This project uses Vite, not Next.js, so the variable names are different. Copy the key values and set them in
.envusing the names below:Clerk Dashboard shows Set in .envasUsed by NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...VITE_CLERK_PUBLISHABLE_KEY=pk_test_...Vite frontend (same publishable key value) CLERK_PUBLISHABLE_KEY=pk_test_...Hono API ( @hono/clerk-auth)CLERK_SECRET_KEY=sk_test_...CLERK_SECRET_KEY=sk_test_...Hono API (JWT verification) The publishable key appears twice with different prefixes:
VITE_is required for Vite to expose it to the browser, and the unprefixedCLERK_PUBLISHABLE_KEYis what the API middleware expects. Use the samepk_test_...value for both. -
The Publishable Key is all you need to get sign-in and sign-up working. The Secret Key is needed for the API to verify JWTs (protect
/api/todos,/api/users, etc.). -
If you can't see the Secret Key, look for a "Reveal" or "Show" button next to it on the API Keys page. Only account Admins can reveal it.
-
Ensure your Clerk application's allowed redirect URLs include
http://localhost:5167(and your production URL when deploying).
Important: When you first open the app, click Sign up to create an account before trying to sign in. There are no existing users until you register one.
Note: The
.envfile lives in the project root (same folder aspackage.json). Vite is configured to load env vars from there viaenvDirinapps/web/vite.config.ts. Do not put your.envinsideapps/web/.
npm run dev- Web app: http://localhost:5167
- API: http://localhost:3834 (e.g. http://localhost:3834/api/health, http://localhost:3834/api/users)
Open the web app URL in your browser. The frontend proxies /api to the API in development, so you don’t need CORS.
With Rust installed:
npm run tauriTauri starts the web app and opens a native window. Build a production desktop binary with npm run tauri:build.
| Command | What it does |
|---|---|
npm run dev |
Runs the web app (5167) and API (3834) together. |
npm run dev:web |
Web app only. |
npm run dev:api |
API only. |
npm run build |
Builds the web app and API. |
npm run lint |
Lints the frontend. |
npm run tauri |
Starts the Tauri dev window (loads the app from 5167). |
npm run tauri:build |
Builds the desktop app. |
npm run db:generate -w @launch-kit-spa-desktop-switchdimension/api |
Generates Drizzle migrations from schema. |
npm run db:migrate -w @launch-kit-spa-desktop-switchdimension/api |
Runs Drizzle migrations. |
npm run db:push -w @launch-kit-spa-desktop-switchdimension/api |
Pushes schema to DB (prototyping). |
npm run db:studio -w @launch-kit-spa-desktop-switchdimension/api |
Opens Drizzle Studio. |
├── apps/
│ ├── web/ # Frontend SPA
│ │ ├── src/
│ │ │ ├── app/ # Layout, sidebar, route config
│ │ │ ├── features/ # Feature modules (add your own)
│ │ │ ├── shared/ # Components, hooks, lib (e.g. API client)
│ │ │ ├── components/ # shadcn/ui components
│ │ │ └── pages/ # Route-level pages
│ │ └── ...
│ └── api/ # Hono API
│ ├── src/
│ │ ├── routes/ # Route modules (e.g. health, users)
│ │ └── lib/
│ │ └── db/ # Drizzle schema and client
│ ├── drizzle/ # Generated migrations
│ └── drizzle.config.ts
├── src-tauri/ # Tauri desktop app (optional)
├── .agents/skills/ # Agent skills (canonical path per [Cursor](https://cursor.com/docs/skills) / [Agent Skills](https://agentskills.io))
├── package.json # Workspaces + root scripts
└── tsconfig.base.json
.agents/skills/— Project-level agent skills (each subdir hasSKILL.md). Use this path only;.agent/skills/(singular) is non-standard and has been removed.app/— Shell: layout, sidebar, and routing. Change once, everything updates.features/— Domain or product features; each can own components, hooks, and state.shared/— Reusable UI and utilities; no imports fromfeaturesorpages.pages/— Thin route components that compose features.
# Build frontend and API
npm run buildDeploy apps/web/dist to any static host or CDN, and run the API (e.g. node dist/index.js from apps/api, or your host’s Node runtime). Or run both in a single Railway (or similar) container.
| Piece | Choice | Why |
|---|---|---|
| Build | Vite | Fast dev server and builds; works the same for web and Tauri. |
| UI | React 19 + TypeScript | Familiar, strong ecosystem; TypeScript keeps the app and API in sync. |
| Styling | Tailwind CSS v4 | Utility-first, quick to refactor; one config for the whole UI. |
| Routing | React Router v7 | SPA routing with a simple, stable API. |
| API | Hono | Small, fast, type-safe; runs on Node now and can move to Cloudflare Workers, Deno, or Bun with a different adapter. |
| Database | Drizzle + PostgreSQL | Type-safe schema and queries; migrations via Drizzle Kit; works with any Postgres (local, Neon, Supabase, etc.). |
| Auth | Clerk | Hosted auth with prebuilt UI components; JWT verification on the API via @hono/clerk-auth. |
| Types | Hono RPC | The API exports its route types; the frontend gets a typed api client with no codegen. |
| Desktop | Tauri v2 | Same HTML/JS/CSS as the web app; small binaries and system access when you need it. |
| UI components | shadcn/ui | Copy-paste components (Radix primitives + Tailwind); theme via CSS variables. |
| Icons | Lucide React | Consistent icon set; used by shadcn and across the app. |
The repo is a monorepo (npm workspaces): frontend and API are separate packages so you can deploy or scale them independently, while still sharing types and running everything with one npm run dev.
I usually reach for Next.js, but I find myself building more and more single-page applications. Next is excellent at static rendering and has a rich ecosystem, but it comes with a lot of APIs and opinions. I want to refactor quickly with AI, host and store the app anywhere, and often ship a desktop build—and I don’t always need the serverless model Next optimizes for. For those cases, a lean SPA fits better.
On the frontend, we use React, Vite, Tailwind, and shadcn/ui. These are widely understood by AI tools and have a strong, predictable developer experience, so refactors and feature work stay fast.
On the backend, we use Hono instead of Express. Hono’s typing and design make it very well suited to working with modern AI: the codebase is easy for models to reason about, and the RPC-style types flow cleanly to the client. We run it on Node for maximum compatibility—Railway, Render, Fly, and almost every other host support it, so you can deploy the API wherever you like. Because Hono is runtime-agnostic, you can later move the same app to Cloudflare Workers (or other edges) for a very cheap, globally distributed backend if that fits your product.
Everything lives in one repository with two clear folders (apps/web and apps/api). You can deploy them separately—frontend to a CDN or static host, API to Node or Workers—or run both in a single Railway (or similar) container: one process serves the API, another serves the built frontend, and they talk over the network. Keeping both sides in one repo means an AI assistant has a complete view of the application and can change frontend and backend together. It also means the API can evolve into a standalone service: other clients (another app, an MCP server, or an agent) can talk to the same backend without being tied to this UI. In an agent-first world, having a first-class, deployable API that isn’t locked to a single framework is important.
The app uses shadcn/ui (new-york style, Radix) and Lucide React for icons. Theme variables live in apps/web/src/index.css (dark by default; add class light on <html> for light mode).
- Add components: From
apps/web, runnpx shadcn@latest add <component>(e.g.button,card,dialog). Components go intosrc/components/ui/. - Use components:
import { Button } from "@/components/ui/button". - Use icons:
import { Home, Settings } from "lucide-react". cn()helper:import { cn } from "@/lib/utils"to merge Tailwind classes.
The app ships with Clerk wired end-to-end:
- Frontend:
ClerkProviderwraps the app inmain.tsx. Routes/sign-inand/sign-uprender Clerk's prebuilt components. All other routes are wrapped in aProtectedRoutethat redirects unauthenticated users to/sign-in. The sidebar shows aUserButtonwhen signed in. - API:
clerkMiddleware()from@hono/clerk-authis applied to all/api/*routes. Protected route handlers (todos, users) callgetAuth(c)and return401when no valid session is present./api/healthremains public. - Authenticated API calls: Use the
useApi()hook from@/shared/lib/use-api— it returns a Hono RPC client that automatically attaches the Clerk session JWT as aBearertoken.
| Variable | Where it's used | Required |
|---|---|---|
VITE_CLERK_PUBLISHABLE_KEY |
Frontend (Vite exposes it to the browser) | Yes — app won't load without it |
CLERK_PUBLISHABLE_KEY |
API (@hono/clerk-auth middleware) |
Yes — API fails silently without it |
CLERK_SECRET_KEY |
API (JWT verification) | Yes — API returns 401 without it |
All three go in the root .env file. VITE_CLERK_PUBLISHABLE_KEY and CLERK_PUBLISHABLE_KEY use the same pk_test_... value.
- No Publishable Key: The app shows a full-page "Clerk not configured" screen with step-by-step setup instructions.
- No Secret Key: The frontend works (sign-in, sign-up, navigation), but all API calls to protected routes return
401 Unauthorized.
The API exports its type; the frontend gets a typed client with no codegen:
// Unauthenticated client (for public routes like /api/health)
import { api } from '@/shared/lib/api-client';
const res = await api.api.health.$get();
const data = await res.json(); // { status: 'ok' } — typed// Authenticated client (for protected routes — use inside React components)
import { useApi } from '@/shared/lib/use-api';
function MyComponent() {
const api = useApi(); // attaches Clerk JWT automatically
// ...
const res = await api.api.todos.$get();
}Add routes in apps/api/src/routes/ and mount them in apps/api/src/index.ts; the client types update automatically.
The API uses Drizzle ORM with PostgreSQL. Schema lives in apps/api/src/lib/db/schema.ts; the client is in apps/api/src/lib/db/index.ts.
- Env: Set
DATABASE_URLin.env(see.env.example). - Schema: Edit
apps/api/src/lib/db/schema.tsand add tables withpgTable,serial,text,timestamp, etc. - Migrations: Run
npm run db:generate -w @launch-kit-spa-desktop-switchdimension/apito generate SQL, thennpm run db:migrateto apply. For quick prototyping, usenpm run db:push. - Studio: Run
npm run db:studio -w @launch-kit-spa-desktop-switchdimension/apito open Drizzle Studio and inspect or edit data. - In routes: Import
dbfrom../lib/db/index.jsand usedb.select(),db.insert(), etc. Example:GET /api/usersinapps/api/src/routes/users.ts.
The same React app runs in a Tauri window. No separate “desktop” UI. Config lives in src-tauri/tauri.conf.json (dev URL, build commands, icons). You need Rust installed to use Tauri.
- Clone or fork this repo and rename it (e.g.
my-product). - Search and replace any remaining project-specific names in:
package.json(name)apps/web/index.html(title)src-tauri/tauri.conf.json(productName, identifier, window title)- Set
DATABASE_URLin.envfor your database.
- Set up Clerk: Create a Clerk application and add
VITE_CLERK_PUBLISHABLE_KEY,CLERK_PUBLISHABLE_KEY, andCLERK_SECRET_KEYto.env(see Set up Clerk for the key name mapping). - Customize the sidebar in
apps/web/src/app/layout/Sidebar.tsx(nav items, branding). - Add routes in
apps/web/src/app/routes.tsxandapps/api/src/routes/, and use the sharedapiclient (oruseApi()for authenticated calls) in the UI. - Optionally remove Tauri if you only need web: delete
src-tauri/, drop@tauri-apps/apifrom the web app, and remove thetauri/tauri:buildscripts from the rootpackage.json.
You can keep the API and run the frontend as a static site, or deploy both; the structure stays the same.
| Service | Port | Override |
|---|---|---|
| Web (Vite) | 5167 | server.port in apps/web/vite.config.ts |
| API (Hono) | 3834 | PORT env var or default in apps/api/src/index.ts |
If you change the API port, update the proxy in apps/web/vite.config.ts and (for production) VITE_API_URL if you use it.
Port already in use (EADDRINUSE)
- Another process is using 5167 or 3834. Stop other dev servers or kill the process:
lsof -ti:5167 | xargs kill lsof -ti:3834 | xargs kill
- Then run
npm run devagain.
API types not resolving in the frontend
- Ensure
npm installhas been run from the repo root so the@launch-kit-spa-desktop-switchdimension/apiworkspace is linked. - The API package exposes
mainandtypesinapps/api/package.jsonpointing at./src/index.ts.
API fails to start or "DATABASE_URL is required"
- Copy
.env.exampleto.envand setDATABASE_URLto a valid PostgreSQL connection string (e.g.postgresql://user:password@localhost:5432/dbname). - Ensure the database exists and migrations have been run (
npm run db:pushornpm run db:migratefrom the API workspace).
"Clerk not configured" screen
- You need
VITE_CLERK_PUBLISHABLE_KEYset in the root.envto a valid Clerk publishable key (starts withpk_test_and has more characters after it). - The
.envfile must be in the project root (next topackage.json), not insideapps/web/. - After editing
.env, restart the dev server (Ctrl+C, thennpm run dev). Vite only reads env files at startup. - Open the browser console (F12 → Console) and look for the
[Clerk config]log to see what value the app received.
API returns 401 on all requests or fails silently
- Make sure all three Clerk variables are set in
.env:VITE_CLERK_PUBLISHABLE_KEY,CLERK_PUBLISHABLE_KEY, andCLERK_SECRET_KEY. The Clerk dashboard showsNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY— copy that value into bothVITE_CLERK_PUBLISHABLE_KEYandCLERK_PUBLISHABLE_KEY. CLERK_PUBLISHABLE_KEY(no prefix) is required by@hono/clerk-auth. If it's missing the API will throw "Missing Clerk Publishable key" on every request.CLERK_SECRET_KEYstarts withsk_test_. Find it on the Clerk Dashboard → API Keys page (click "Reveal" or "Show"; only Admins can see it).- Restart the dev server after editing
.env.
Tauri dev/build fails
- Install Rust: tauri.app/start/install.
- Ensure the web app builds:
npm run build --workspace=@launch-kit-spa-desktop-switchdimension/web.