Skip to content

Dashboard Tutorial: Setup

Build an authenticated dashboard with interactive islands.

What We're Building

A team dashboard with:

  • User authentication (sessions)
  • Protected routes
  • Interactive widgets (islands)
  • Real-time data updates

Prerequisites

Create the Project

bash
bunx create-ereo@latest dashboard-app --template minimal
cd dashboard-app

Note: We use --template minimal to start with a clean project structure.

Install Dependencies

bash
bun add bcrypt @ereo/state
bun add -d @types/bcrypt

Note: We install @ereo/state for shared signals between dashboard islands (used in Chapter 3 and 4).

Project Structure

dashboard-app/
├── app/
│   ├── routes/
│   │   ├── _middleware.ts      # Auth middleware (all routes)
│   │   ├── _layout.tsx         # Root layout
│   │   ├── index.tsx           # Landing page
│   │   ├── login.tsx           # Login page
│   │   ├── register.tsx        # Registration
│   │   └── dashboard/
│   │       ├── _middleware.ts  # Protected route middleware
│   │       ├── _layout.tsx     # Dashboard layout
│   │       ├── index.tsx       # Dashboard home
│   │       ├── analytics.tsx   # Analytics page
│   │       └── settings.tsx    # User settings
│   ├── components/
│   │   ├── StatsWidget.tsx     # Interactive stats
│   │   ├── ActivityFeed.tsx    # Live activity
│   │   └── ChartWidget.tsx     # Data visualization
│   └── lib/
│       ├── auth.ts             # Auth utilities
│       └── db.ts               # Database setup
├── ereo.config.ts
└── package.json

Database Setup

Create app/lib/db.ts:

ts
import { Database } from 'bun:sqlite'

const db = new Database('dashboard.db')

// Initialize tables
db.run(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    password TEXT NOT NULL,
    name TEXT NOT NULL,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  )
`)

db.run(`
  CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    expires_at DATETIME NOT NULL,
    FOREIGN KEY (user_id) REFERENCES users(id)
  )
`)

db.run(`
  CREATE TABLE IF NOT EXISTS activities (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    action TEXT NOT NULL,
    metadata TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
  )
`)

export { db }

Auth Utilities

Create app/lib/auth.ts:

ts
import { db } from './db'
import bcrypt from 'bcrypt'
import { randomUUID } from 'crypto'

interface User {
  id: number
  email: string
  name: string
}

export async function createUser(email: string, password: string, name: string): Promise<User> {
  const hashedPassword = await bcrypt.hash(password, 10)

  const result = db.run(
    'INSERT INTO users (email, password, name) VALUES (?, ?, ?)',
    [email, hashedPassword, name]
  )

  return {
    id: result.lastInsertRowid as number,
    email,
    name
  }
}

export async function verifyCredentials(email: string, password: string): Promise<User | null> {
  const user = db.query<{ id: number; email: string; password: string; name: string }, [string]>(
    'SELECT * FROM users WHERE email = ?'
  ).get(email)

  if (!user) return null

  const valid = await bcrypt.compare(password, user.password)
  if (!valid) return null

  return { id: user.id, email: user.email, name: user.name }
}

export function createSession(userId: number): string {
  const sessionId = randomUUID()
  const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 days

  db.run(
    'INSERT INTO sessions (id, user_id, expires_at) VALUES (?, ?, ?)',
    [sessionId, userId, expiresAt.toISOString()]
  )

  return sessionId
}

export function getSession(sessionId: string): User | null {
  const result = db.query<{ user_id: number; email: string; name: string; expires_at: string }, [string]>(`
    SELECT s.user_id, u.email, u.name, s.expires_at
    FROM sessions s
    JOIN users u ON s.user_id = u.id
    WHERE s.id = ?
  `).get(sessionId)

  if (!result) return null
  if (new Date(result.expires_at) < new Date()) {
    db.run('DELETE FROM sessions WHERE id = ?', [sessionId])
    return null
  }

  return { id: result.user_id, email: result.email, name: result.name }
}

export function deleteSession(sessionId: string): void {
  db.run('DELETE FROM sessions WHERE id = ?', [sessionId])
}

Auth Middleware

EreoJS uses file-based middleware via _middleware.ts files in the routes directory. Middleware applies to all routes at the same level and below.

Create app/routes/_middleware.ts to extract the session for all routes:

ts
// app/routes/_middleware.ts
import type { MiddlewareHandler } from '@ereo/core'
import { getSession } from '~/lib/auth'

const authMiddleware: MiddlewareHandler = async (request, context, next) => {
  const cookies = request.headers.get('cookie') || ''
  const sessionId = cookies.match(/session=([^;]+)/)?.[1]

  if (sessionId) {
    const user = getSession(sessionId)
    if (user) {
      context.set('user', user)
    }
  }

  return next()
}

export default authMiddleware

Create app/routes/dashboard/_middleware.ts to protect all dashboard routes:

ts
// app/routes/dashboard/_middleware.ts
import type { MiddlewareHandler } from '@ereo/core'

const requireAuth: MiddlewareHandler = async (request, context, next) => {
  const user = context.get('user')

  if (!user) {
    return Response.redirect('/login?redirect=' + encodeURIComponent(new URL(request.url).pathname))
  }

  return next()
}

export default requireAuth

Next Steps

In the next chapter, we'll build the authentication pages:

  • Login form with validation
  • Registration with error handling
  • Session management

Continue to Chapter 2: Authentication →

Released under the MIT License.