rdev/examples/dashboard-app
jordan 62460bf098 feat: complete template upgrade - chassis framework, UI library, auth, app-nextjs, OpenAPI, and cookbook
Weeks 1-7 of the template upgrade plan:
- pkg/api: typed HTTPError with sentinels, Wrap/WrapMiddleware, Bind, health probes, OpenAPI schema/param builders
- skeleton/packages: ui (design tokens, components), layout (DashboardShell), auth (AuthProvider, ProtectedRoute), api-client
- skeleton/pkg: httperror, app/handler, app/bind, app/health, auth (JWT/API key middleware)
- components/app-nextjs: Next.js 14 App Router template with dashboard, server actions, auth
- cookbooks/feature-development.md with test and validation scripts
- Handler tests for components, project management, and woodpecker webhook
- 3 rounds of code review fixes applied

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-02 00:46:51 -07:00
..
README.md feat: complete template upgrade - chassis framework, UI library, auth, app-nextjs, OpenAPI, and cookbook 2026-02-02 00:46:51 -07:00

Dashboard App Example

This example demonstrates a full-stack composable monorepo project using rdev's enhanced templates with:

  • Chassis patterns: Wrap, Bind, HTTPError, Health probes
  • Design system: packages/ui, packages/layout
  • Authentication: pkg/auth, packages/auth
  • OpenAPI: Auto-generated TypeScript client
  • Next.js 14: App Router with server actions

Creating This Example

# 1. Create the project
curl -X POST "$RDEV_API_URL/project" \
  -H "X-API-Key: $RDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dashboard-app",
    "description": "Example dashboard application"
  }'

# 2. Add the API service
curl -X POST "$RDEV_API_URL/projects/dashboard-app/components" \
  -H "X-API-Key: $RDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "service",
    "name": "api"
  }'

# 3. Add the Next.js dashboard
curl -X POST "$RDEV_API_URL/projects/dashboard-app/components" \
  -H "X-API-Key: $RDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "app-nextjs",
    "name": "dashboard"
  }'

Project Structure

After creation, the project looks like:

dashboard-app/
├── CLAUDE.md                        # AI routing with guides
├── README.md                        # Project docs
├── go.work                          # Go workspace
├── pnpm-workspace.yaml              # pnpm monorepo config
├── .woodpecker.yml                  # CI pipeline
│
├── pkg/                             # Shared Go packages
│   ├── app/                         # Wrap, Bind, Health
│   │   ├── app.go                   # NewApp, Run
│   │   ├── handler.go               # Wrap pattern
│   │   ├── bind.go                  # BindAndValidate
│   │   └── health.go                # Health probes
│   ├── httperror/                   # Typed HTTP errors
│   │   └── error.go                 # Sentinels, factory functions
│   ├── httpresponse/                # JSON envelope
│   ├── httpvalidation/              # Validator wrapper
│   ├── middleware/                  # HTTP middleware
│   └── auth/                        # JWT + API key auth
│       ├── auth.go                  # Interface, types
│       ├── context.go               # GetUser, SetUser
│       ├── jwt.go                   # JWTValidator
│       ├── apikey.go                # APIKeyValidator
│       └── middleware.go            # Auth middleware
│
├── packages/                        # Shared TypeScript packages
│   ├── ui/                          # Design system
│   │   ├── src/styles/tokens.css    # CSS custom properties
│   │   ├── src/utils/cn.ts          # clsx + tailwind-merge
│   │   └── src/components/          # Button, Card, Input, etc.
│   ├── layout/                      # Shell components
│   │   ├── src/DashboardShell.tsx   # Main layout
│   │   ├── src/Sidebar.tsx          # Navigation
│   │   └── src/Header.tsx           # Top bar
│   ├── auth/                        # React auth
│   │   ├── src/AuthProvider.tsx     # Context provider
│   │   ├── src/useAuth.ts           # Hook
│   │   └── src/ProtectedRoute.tsx   # Route guard
│   ├── logger/                      # Structured logging
│   └── api-client/                  # Generated client
│       └── src/schema.d.ts          # TypeScript types
│
├── services/                        # Backend services
│   └── api/
│       ├── cmd/server/main.go       # Entry point
│       ├── internal/
│       │   ├── api/routes.go        # Chi routes with Wrap
│       │   ├── config/config.go     # Configuration
│       │   └── domain/              # Business models
│       ├── migrations/              # SQL migrations
│       ├── Dockerfile               # Multi-stage Go build
│       └── component.yaml           # Port, dependencies
│
└── apps/                            # Frontend applications
    └── dashboard/
        ├── src/app/
        │   ├── layout.tsx           # Root layout
        │   ├── globals.css          # Design tokens import
        │   ├── (dashboard)/         # Protected route group
        │   │   ├── layout.tsx       # DashboardShell
        │   │   ├── page.tsx         # Home
        │   │   └── settings/page.tsx
        │   └── login/page.tsx       # Public login
        ├── src/components/
        │   └── providers.tsx        # AuthProvider wrapper
        ├── src/actions/             # Server actions
        ├── Dockerfile               # Next.js standalone
        └── component.yaml

Development Workflow

Local Development

# Start infrastructure (Postgres, Redis)
docker-compose up -d

# Install dependencies
./scripts/install.sh

# Start all services
./scripts/dev.sh

Building a Feature

  1. Create feature branch

    git checkout -b feature/user-profile
    
  2. Add API endpoint using Wrap + Bind:

    func (h *Handler) GetMe() http.HandlerFunc {
        return app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
            user, ok := auth.GetUser(r.Context())
            if !ok {
                return httperror.Unauthorized("not authenticated")
            }
            profile, err := h.svc.GetByID(r.Context(), user.ID)
            if err != nil {
                return httperror.WrapError(err, "failed to get profile")
            }
            httpresponse.JSON(w, http.StatusOK, profile)
            return nil
        })
    }
    
  3. Regenerate TypeScript client:

    ./scripts/generate-client.sh
    
  4. Add frontend page using design system:

    import { Card, CardHeader, CardTitle } from '@dashboard-app/ui';
    import { useAuth } from '@dashboard-app/auth';
    
    export default function ProfilePage() {
        const { user } = useAuth();
        return (
            <Card>
                <CardHeader>
                    <CardTitle>Profile: {user?.name}</CardTitle>
                </CardHeader>
            </Card>
        );
    }
    
  5. Run tests and quality checks:

    ./scripts/quality.sh
    
  6. Commit and push:

    git add -A
    git commit -m "feat: add user profile"
    git push -u origin feature/user-profile
    

Key Patterns

Backend: Wrap Pattern

// Error-returning handlers become http.HandlerFunc
app.Wrap(func(w http.ResponseWriter, r *http.Request) error {
    // Return errors - they become proper HTTP responses
    if err := doThing(); err != nil {
        return httperror.Internal("something went wrong")
    }
    httpresponse.JSON(w, http.StatusOK, data)
    return nil
})

Backend: Bind Pattern

// Decode + validate in one call
var req CreateItemRequest
if err := app.BindAndValidate(r, &req); err != nil {
    return err // Validation errors become 400 with details
}

Frontend: Auth Hook

const { user, isLoading, login, logout } = useAuth();

if (isLoading) return <Spinner />;
if (!user) return <LoginRedirect />;

Frontend: Design System

import { Button, Card, Input, Badge } from '@dashboard-app/ui';
import { DashboardShell, Sidebar } from '@dashboard-app/layout';

Deployment

CI/CD is automated via Woodpecker:

  1. Push triggers build
  2. Kaniko builds Docker images
  3. Images pushed to registry
  4. kubectl deploys to K8s

Access the deployed app:

open https://<slug>.threesix.ai

Cleanup

curl -X DELETE "$RDEV_API_URL/project/dashboard-app" \
  -H "X-API-Key: $RDEV_API_KEY"