Postgres · React · Elysia · Vike, with Drizzle, Better Auth and shadcn/ui.
A web app starter: landing page, email authentication, a protected dashboard, a working CRUD example, and a Docker setup.
- Landing page at
/, login and signup at/loginand/signup, both with email + password via Better Auth, session cookies, and server-side route guards. - Dashboard at
/dashboardwith a collapsible sidebar, stat cards, an interactive chart, and a data table (shadcn/ui blocks). - Live CRUD example at
/dashboard/products, with a Postgres-backed table, a dialog form, toast feedback, and seed data. This is the pattern to copy for every new entity. - PostgreSQL + Drizzle, versioned migrations, type-safe queries.
- Docker setup (multi-stage image + Compose, app + Postgres, migrations applied automatically on start).
- React Compiler on, automatic memoization, no manual
useMemo. oxlint(type-aware) andtsc --noEmitboth run clean.
Requirements: Bun, PostgreSQL 17+ (or Docker).
cp .env.example .env # fill in DATABASE_URL + BETTER_AUTH_SECRET (generate: bunx @better-auth/cli secret)
bun install
bun run drizzle:migrate
bun run db:seed # optional example products
bun run dev # → http://localhost:3000docker compose up --build # → http://localhost:3000 (migrations applied automatically)| Command | Description |
|---|---|
bun run dev |
Vike dev server (default port 3000) |
bun run build / bun run prod |
Production build / run built server |
bun run lint |
oxlint (type-aware, must be 0 errors) |
bunx tsc --noEmit |
Typecheck |
bun run drizzle:generate |
Generate migration from schema changes |
bun run drizzle:migrate |
Apply pending migrations |
bun run drizzle:studio |
Drizzle Studio DB browser |
bun run db:seed |
Seed example products (idempotent) |
src/
├── pages/ # Filesystem routing, folder name = URL
│ ├── index/ # / landing page
│ ├── login/ # /login (+data.ts redirects to /dashboard when logged in)
│ ├── signup/ # /signup (idem)
│ ├── dashboard/ # /dashboard (+Layout sidebar shell, +data.ts auth guard)
│ │ └── products/ # /dashboard/products live CRUD example
│ ├── _error/ # 404 / 500 page
│ └── +Layout.tsx # Minimal root layout (pages bring their own shell)
├── components/
│ ├── ui/ # shadcn/ui primitives (button, card, table, sidebar, chart, dialog, …)
│ └── app-sidebar.tsx, data-table.tsx, login-form.tsx, … # blocks + wired forms
├── server/
│ ├── auth.ts # Better Auth instance (email+password, Drizzle adapter)
│ ├── elysia.ts # App wiring: /api/auth/* (via onRequest) + API routes + Vike
│ ├── products.ts # Template CRUD routes (session guard + Elysia validation)
│ └── db-middleware.ts / load.ts # pageContext.db + .env loading
├── database/drizzle/
│ ├── schema/ # auth.ts (Better Auth tables) and products.ts
│ ├── queries/ # Type-safe query functions per entity
│ └── migrations/ # Versioned SQL (never edit applied files)
├── lib/ # auth-client.ts (useSession/signIn/signUp/signOut) · form.ts · utils.ts
└── +server.ts # Server entry (PORT env, default 3000)
| URL | Access | Description |
|---|---|---|
/ |
public | Landing page |
/login, /signup |
public (bounce when logged in) | Auth forms with inline errors |
/dashboard |
login required | Sidebar + stat cards + chart + sample data table |
/dashboard/products |
login required | Live product CRUD backed by Postgres |
/api/auth/* |
n/a | Better Auth endpoints (sign-up/in/out, session) |
/api/products* |
login required | Product CRUD API (401 anonymous, 422 invalid body) |
| Variable | Description |
|---|---|
DATABASE_URL |
Postgres connection string |
BETTER_AUTH_SECRET |
Auth signing secret (bunx @better-auth/cli secret) |
BETTER_AUTH_URL |
Canonical app URL (default http://localhost:3000) |
TRUSTED_ORIGINS |
Comma-separated extra origins allowed to call the auth API (e.g. dev on another port) |
PORT |
Server port (default 3000) |
POSTGRES_PASSWORD / POSTGRES_DB |
Used by docker-compose.yml for the db service |
PUBLIC_ENV__GOOGLE_ANALYTICS |
Optional GA measurement ID (client-exposed) |
Copy the products flow. Five steps, no new concepts:
- Schema: add
src/database/drizzle/schema/<entity>.ts. Seeproducts.tsfor the convention (text PK viacrypto.randomUUID(), integer minor units for money). - Migrate:
bun run drizzle:generate && bun run drizzle:migrate. - Queries: add
src/database/drizzle/queries/<entity>.tswith list/create/update/delete functions on a module-leveldbsingleton. - API: register routes in
src/server/followingproducts.ts. UseonBeforeHandlefor the 401 session check and Elysiatschemas for the 422 validation. - Page: add
src/pages/dashboard/<entity>/+Page.tsxfollowingproducts/+Page.tsx: fetch the list,Dialogform,toastfeedback. The auth guard is inherited fromdashboard/+data.ts.
bunx shadcn@latest add <component> # e.g. dialog, alert-dialog, calendarAliases (@/* → src/*) are configured in tsconfig.json, vite.config.ts, and components.json, so registry imports work unchanged. Blocks (dashboard-01, login-02, etc.) may bring "use client" directives, harmless under Vike, leave them.
- Better Auth 1.7 requires
account.issuerplus a unique index. The bundledbetter-auth generateCLI does not emit it.src/database/drizzle/schema/auth.tsalready includes the manual fix. Recheck this when upgradingbetter-auth. - Elysia consumes request bodies before route handlers run. That's why
/api/auth/*is forwarded inonRequest(seesrc/server/elysia.ts), not via.all(). Normal API routes are unaffected. - Dev server restarts do not reload
process.env. After editing.env, kill and restartbun run devinstead of relying on Vite auto-restart. - Money is stored as integer minor units (see
product.price) to avoid float rounding. dist/,node_modules/, and.envare gitignored. Migrations are committed. Never edit an applied migration, always generate a new one.
Vike · React 19 · Elysia · PostgreSQL · Drizzle ORM · Better Auth · Tailwind CSS v4 · shadcn/ui · Recharts · TanStack Table · Bun · TypeScript · oxlint