Computer
macOS, Windows, or Linux with admin access
A complete, beginner-friendly guide to install and configure the modern dashboard tech stack. This guide will take you from zero to a fully functional dashboard with React 19, Cloudflare Workers, Neon PostgreSQL, and more.
Before starting, ensure you have:
Computer
macOS, Windows, or Linux with admin access
Internet
Stable internet connection for downloads
Accounts
Free accounts: GitHub, Cloudflare, Neon (we’ll create these)
Time
~45-60 minutes to complete full setup
Bun is our primary build tool and package manager - it’s 4x faster than npm!
Install Bun
curl -fsSL https://bun.sh/install | bashpowershell -c "irm bun.sh/install.ps1 | iex"Verify Installation
bun --versionYou should see something like: 1.0.0 or higher
Restart Your Terminal
Close and reopen your terminal to ensure bun is in your PATH
Git is required for version control and some tools we’ll use.
# Using Homebrew (recommended)brew install git
# Or use Xcode Command Line Toolsxcode-select --installDownload from git-scm.com and run the installer.
Use default settings during installation.
# Ubuntu/Debiansudo apt updatesudo apt install git
# Fedorasudo dnf install gitVerify Git:
git --versionWhile you can use any editor, VS Code has the best support for our stack.
Install Recommended Extensions:
# Open VS Code and install these extensionscode --install-extension dbaeumer.vscode-eslintcode --install-extension esbenp.prettier-vscodecode --install-extension bradlc.vscode-tailwindcsscode --install-extension astro-build.astro-vscodemy-dashboard-dbOpen Terminal in your projects folder
Create React Project with Vite
bun create vite my-dashboard --template react-tsNavigate to Project
cd my-dashboardInstall Dependencies
bun installTest Development Server
bun run devVisit http://localhost:5173 - you should see the Vite + React page!
Stop the server (Press Ctrl+C)
# Routingbun add @tanstack/react-routerbun add -D @tanstack/router-vite-plugin
# Data Fetching & Statebun add @tanstack/react-querybun add zustand
# Forms & Validationbun add react-hook-form zod @hookform/resolvers# Tailwind CSSbun add -D tailwindcss postcss autoprefixerbunx tailwindcss init -p
# shadcn/ui dependenciesbun add tailwindcss-animate class-variance-authority clsx tailwind-mergebun add lucide-react# Charts & Tablesbun add recharts @tremor/reactbun add @tanstack/react-tableUpdate tailwind.config.js
/** @type {import('tailwindcss').Config} */export default { darkMode: ["class"], content: [ './pages/**/*.{ts,tsx}', './components/**/*.{ts,tsx}', './app/**/*.{ts,tsx}', './src/**/*.{ts,tsx}', ], theme: { extend: { colors: { border: "hsl(var(--border))", input: "hsl(var(--input))", ring: "hsl(var(--ring))", background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", primary: { DEFAULT: "hsl(var(--primary))", foreground: "hsl(var(--primary-foreground))", }, secondary: { DEFAULT: "hsl(var(--secondary))", foreground: "hsl(var(--secondary-foreground))", }, }, borderRadius: { lg: "var(--radius)", md: "calc(var(--radius) - 2px)", sm: "calc(var(--radius) - 4px)", }, }, }, plugins: [require("tailwindcss-animate")],}Update src/index.css
@tailwind base;@tailwind components;@tailwind utilities;
@layer base { :root { --background: 0 0% 100%; --foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; --secondary: 210 40% 96.1%; --secondary-foreground: 222.2 47.4% 11.2%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; --radius: 0.5rem; }
.dark { --background: 222.2 84% 4.9%; --foreground: 210 40% 98%; --primary: 210 40% 98%; --primary-foreground: 222.2 47.4% 11.2%; --secondary: 217.2 32.6% 17.5%; --secondary-foreground: 210 40% 98%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 83.9%; }}Initialize shadcn/ui
bunx shadcn-ui@latest initAnswer the prompts:
Add Essential Components
bunx shadcn-ui@latest add buttonbunx shadcn-ui@latest add cardbunx shadcn-ui@latest add inputbunx shadcn-ui@latest add labelbunx shadcn-ui@latest add tablebunx shadcn-ui@latest add dialogbunx shadcn-ui@latest add dropdown-menu# Drizzle ORMbun add drizzle-orm @neondatabase/serverlessbun add -D drizzle-kitCreate drizzle.config.ts in project root
import type { Config } from 'drizzle-kit';
export default { schema: './src/db/schema.ts', out: './drizzle', driver: 'pg', dbCredentials: { connectionString: process.env.DATABASE_URL!, },} satisfies Config;Create .env file in project root
DATABASE_URL=postgresql://user:password@ep-xyz.region.aws.neon.tech/dbnameAdd .env to .gitignore
echo ".env" >> .gitignoreCreate folder structure
mkdir -p src/dbCreate src/db/schema.ts
import { pgTable, text, serial, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', { id: serial('id').primaryKey(), name: text('name').notNull(), email: text('email').notNull().unique(), createdAt: timestamp('created_at').defaultNow(),});
export const posts = pgTable('posts', { id: serial('id').primaryKey(), title: text('title').notNull(), content: text('content'), userId: serial('user_id').references(() => users.id), createdAt: timestamp('created_at').defaultNow(),});Generate Migration
bunx drizzle-kit generate:pgPush to Database
bunx drizzle-kit push:pgCreate src/db/client.ts:
import { drizzle } from 'drizzle-orm/neon-http';import { neon } from '@neondatabase/serverless';import * as schema from './schema';
const sql = neon(import.meta.env.VITE_DATABASE_URL!);export const db = drizzle(sql, { schema });# Create API foldermkdir apicd api
# Initialize Hono projectbun create hono my-apicd my-api
# Install dependenciesbun install# Add Better Auth for authenticationbun add better-auth
# Add database supportbun add drizzle-orm @neondatabase/serverless
# Add Cloudflare Workers supportbun add -D wranglerCreate src/index.ts:
import { Hono } from 'hono'import { cors } from 'hono/cors'
const app = new Hono()
// Enable CORS for frontendapp.use('/*', cors())
// Health checkapp.get('/', (c) => { return c.json({ message: 'Dashboard API' })})
// Users endpointapp.get('/api/users', async (c) => { // Database query will go here return c.json({ users: [] })})
// Posts endpointapp.get('/api/posts', async (c) => { return c.json({ posts: [] })})
export default appCreate wrangler.toml:
name = "my-dashboard-api"main = "src/index.ts"compatibility_date = "2025-01-13"
[vars]ENVIRONMENT = "production"bun run devVisit http://localhost:8787 - you should see {"message": "Dashboard API"}!
Create src/lib/query-client.ts:
import { QueryClient } from '@tanstack/react-query';
export const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 60 * 1000, // 1 minute retry: 1, }, },});Update src/main.tsx:
import React from 'react'import ReactDOM from 'react-dom/client'import { QueryClientProvider } from '@tanstack/react-query'import { queryClient } from './lib/query-client'import App from './App.tsx'import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <QueryClientProvider client={queryClient}> <App /> </QueryClientProvider> </React.StrictMode>,)Create src/lib/api.ts:
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8787';
export async function fetchUsers() { const res = await fetch(`${API_URL}/api/users`); if (!res.ok) throw new Error('Failed to fetch users'); return res.json();}
export async function fetchPosts() { const res = await fetch(`${API_URL}/api/posts`); if (!res.ok) throw new Error('Failed to fetch posts'); return res.json();}Create src/components/Dashboard.tsx:
import { useQuery } from '@tanstack/react-query';import { fetchUsers } from '../lib/api';import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
export function Dashboard() { const { data, isLoading } = useQuery({ queryKey: ['users'], queryFn: fetchUsers, });
if (isLoading) return <div>Loading...</div>;
return ( <div className="p-8"> <h1 className="text-3xl font-bold mb-6">Dashboard</h1>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3"> <Card> <CardHeader> <CardTitle>Total Users</CardTitle> </CardHeader> <CardContent> <p className="text-3xl font-bold">{data?.users?.length || 0}</p> </CardContent> </Card>
<Card> <CardHeader> <CardTitle>Active Projects</CardTitle> </CardHeader> <CardContent> <p className="text-3xl font-bold">0</p> </CardContent> </Card>
<Card> <CardHeader> <CardTitle>Revenue</CardTitle> </CardHeader> <CardContent> <p className="text-3xl font-bold">$0</p> </CardContent> </Card> </div> </div> );}import { Dashboard } from './components/Dashboard'
function App() { return <Dashboard />}
export default App# In your API projectcd api/my-apibun add better-authCreate src/lib/auth.ts:
import { betterAuth } from 'better-auth';
export const auth = betterAuth({ database: { provider: 'pg', url: process.env.DATABASE_URL!, }, emailAndPassword: { enabled: true, },});Update src/index.ts:
import { Hono } from 'hono'import { auth } from './lib/auth'
const app = new Hono()
// Mount auth routesapp.use('/api/auth/*', async (c) => { return auth.handler(c.req.raw)})
// ... rest of your routesLogin to Cloudflare
cd api/my-apibunx wrangler loginAdd Environment Variables
bunx wrangler secret put DATABASE_URL# Paste your Neon connection stringDeploy
bunx wrangler deploySave Your API URL
Copy the URL from the output: https://my-dashboard-api.YOUR-SUBDOMAIN.workers.dev
Build Your Frontend
cd my-dashboardbun run buildInstall Wrangler
bun add -D wranglerDeploy to Pages
bunx wrangler pages publish distYour Dashboard is Live!
You’ll get a URL like: https://my-dashboard.pages.dev
Sign up at clickhouse.com/cloud
Install ClickHouse Client
bun add @clickhouse/clientCreate Analytics Schema
CREATE TABLE analytics.events ( event_name String, user_id UInt64, timestamp DateTime, properties String) ENGINE = MergeTree()ORDER BY (timestamp, event_name);Track Events in Your App
import { createClient } from '@clickhouse/client';
const client = createClient({ host: process.env.CLICKHOUSE_HOST, password: process.env.CLICKHOUSE_PASSWORD,});
await client.insert({ table: 'analytics.events', values: [{ event_name: 'page_view', user_id: 123, timestamp: new Date(), properties: JSON.stringify({ page: '/dashboard' }) }]});Sign up at sentry.io
Install Sentry
bun add @sentry/reactInitialize Sentry
import * as Sentry from "@sentry/react";
Sentry.init({ dsn: "YOUR_SENTRY_DSN", environment: "production",});Make sure everything works:
Frontend Development Server
cd my-dashboardbun run dev✅ Opens at http://localhost:5173
API Development Server
cd api/my-apibun run dev✅ Opens at http://localhost:8787
Database Connection ✅ Can query Neon database ✅ Tables exist in Neon dashboard
UI Components ✅ shadcn/ui components render correctly ✅ Tailwind CSS styles applied ✅ Dark mode works
Production Deployment ✅ API deployed to Cloudflare Workers ✅ Frontend deployed to Cloudflare Pages ✅ Environment variables configured
Now that your dashboard is set up, you can:
You’ve successfully installed and configured:
✅ Frontend: React 19 + TypeScript + Tailwind CSS + shadcn/ui ✅ Backend: Hono API on Cloudflare Workers ✅ Database: Neon PostgreSQL with Drizzle ORM ✅ State Management: TanStack Query + Zustand ✅ Authentication: Better Auth ✅ Deployment: Cloudflare Workers + Pages
Your dashboard is production-ready! 🚀
Last updated: October 2025