A minimal, blazing-fast starter optimized for agentic AI tools like Lovable, Bolt, Cursor, and more.
| Technology | Purpose |
|---|---|
| Vite | ~300ms dev server, instant HMR |
| React 19 | Latest React |
| TanStack Router | Type-safe file-based routing |
| TanStack Query | Async state management |
| Supabase | Auth, database, edge functions (optional) |
| Tailwind CSS v4 | Utility-first styling |
| shadcn/ui | Accessible component library |
npm install
npm run devβββ src/
β βββ components/ # Reusable components
β β βββ ui/ # shadcn/ui components
β β βββ error-boundary.tsx
β β βββ theme-provider.tsx
β β βββ theme-toggle.tsx
β βββ hooks/ # Custom React hooks
β βββ lib/ # Utilities and clients
β β βββ supabase.ts # Supabase client (optional)
β β βββ query-client.ts # TanStack Query config
β β βββ utils.ts # cn() helper
β βββ routes/ # File-based routes
β β βββ __root.tsx # Root layout (providers)
β β βββ _404.tsx # Not found page
β β βββ index.tsx # /
β β βββ examples.tsx # /examples (Supabase patterns)
β βββ main.tsx # Entry point
βββ supabase/
βββ functions/ # Edge Functions (Deno)
βββ hello/ # Example function
These conventions help AI agents understand and extend this codebase consistently.
Create a file in src/routes/:
// src/routes/dashboard.tsx β /dashboard
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/dashboard')({
component: DashboardPage,
})
function DashboardPage() {
return <div>Dashboard</div>
}Dynamic routes:
// src/routes/users/$userId.tsx β /users/:userId
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/users/$userId')({
component: UserPage,
})
function UserPage() {
const { userId } = Route.useParams()
return <div>User {userId}</div>
}Nested layouts:
// src/routes/_dashboard.tsx β Layout for /dashboard/*
// src/routes/_dashboard/index.tsx β /dashboard
// src/routes/_dashboard/settings.tsx β /dashboard/settingsUse TanStack Query for all data fetching:
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
function MyComponent() {
const { data, isLoading } = useQuery({
queryKey: ['items'],
queryFn: () => fetch('/api/items').then(r => r.json()),
})
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (newItem) => fetch('/api/items', {
method: 'POST',
body: JSON.stringify(newItem),
}),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['items'] }),
})
}Use shadcn/ui CLI:
npx shadcn@latest add dialog
npx shadcn@latest add dropdown-menuComponents are added to src/components/ui/.
Place in src/hooks/ and export from src/hooks/index.ts:
// src/hooks/use-something.ts
export function useSomething() {
// ...
}
// src/hooks/index.ts
export { useSomething } from './use-something'import { toast } from 'sonner'
toast.success('Saved!')
toast.error('Something went wrong')
toast.loading('Saving...')import { ThemeToggle } from '@/components/theme-toggle'
import { useTheme } from '@/components/theme-provider'
// In a component
const { theme, setTheme, resolvedTheme } = useTheme()import { supabase, isSupabaseConfigured } from '@/lib/supabase'
if (isSupabaseConfigured()) {
const { data } = await supabase.from('table').select('*')
}Set in .env:
VITE_SUPABASE_URL=...
VITE_SUPABASE_ANON_KEY=...
// src/routes/_authenticated.tsx
import { createFileRoute, redirect } from '@tanstack/react-router'
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async () => {
const session = await getSession()
if (!session) throw redirect({ to: '/login' })
},
component: AuthenticatedLayout,
})npm install react-hook-form @hookform/resolvers zodimport { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
})
function MyForm() {
const form = useForm({
resolver: zodResolver(schema),
})
return (
<form onSubmit={form.handleSubmit(onSubmit)}>
<Input {...form.register('email')} />
</form>
)
}// src/lib/api/items.ts
import { supabase } from '@/lib/supabase'
export const itemsApi = {
list: () => supabase!.from('items').select('*'),
get: (id: string) => supabase!.from('items').select('*').eq('id', id).single(),
create: (data: CreateItem) => supabase!.from('items').insert(data),
update: (id: string, data: UpdateItem) => supabase!.from('items').update(data).eq('id', id),
delete: (id: string) => supabase!.from('items').delete().eq('id', id),
}For backend logic, use Supabase Edge Functions (Deno-based):
# Install Supabase CLI
npm install -g supabase
# Create a new function
supabase functions new hello
# Deploy
supabase functions deploy hello// supabase/functions/hello/index.ts
Deno.serve(async (req) => {
const { name } = await req.json()
return new Response(
JSON.stringify({ message: `Hello ${name}!` }),
{ headers: { 'Content-Type': 'application/json' } }
)
})Call from client:
const { data } = await supabase.functions.invoke('hello', {
body: { name: 'World' },
})useEffect(() => {
if (!supabase) return
const channel = supabase
.channel('todos')
.on(
'postgres_changes',
{ event: '*', schema: 'public', table: 'todos' },
(payload) => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
}
)
.subscribe()
return () => {
supabase.removeChannel(channel)
}
}, [])# Development
npm run dev # Start dev server
npm run build # Production build
npm run preview # Preview build
npm run lint # ESLint
npm run typecheck # TypeScript check
npm run clean # Clear build cache
# Supabase (requires CLI: npm i -g supabase)
npm run supabase:start # Start local Supabase
npm run supabase:stop # Stop local Supabase
npm run supabase:status # Check status
npm run supabase:gen-types # Generate TS types from DB
npm run supabase:deploy # Deploy all Edge Functions
npm run supabase:deploy:hello # Deploy hello functionBefore using Supabase scripts, set your project ID in package.json:
"config": {
"supabase_project_id": "your-project-id"
}