v0.dev Project Specification - TVL Platform
Complete reference for using v0.dev to build TVL Platform admin dashboard
Purpose: This document is the "source of truth" to feed v0.dev when generating UI components. Copy relevant sections into v0.dev prompts to ensure consistency with our architecture.
Quick Start
When prompting v0.dev, include this context:
I'm building a vacation rental management platform (TVL Platform).
Tech Stack:
- Next.js 14 App Router
- React 18
- TypeScript (strict mode)
- Tailwind CSS
- Shadcn UI components
- Supabase for database (PostgreSQL)
- Cookie-based authentication
Multi-tenant architecture:
- Every table has org_id (organization) and account_id (sub-tenant)
- Data must be filtered by current organization
Follow these constraints:
[Paste relevant sections below]
Table of Contents
- Tech Stack Constraints
- Database Connection
- Authentication Patterns
- Multi-Tenancy Patterns
- API Integration
- File Structure
- Code Patterns
- Standard Prompts
Tech Stack Constraints
Framework & Language
REQUIRED:
- Next.js 14.x with App Router (app/ directory)
- React 18.x
- TypeScript 5.3+ with strict mode
- No JavaScript files (everything must be .ts or .tsx)
FORBIDDEN:
- Pages Router (pages/ directory)
- Class components
- PropTypes (use TypeScript types)
- Any typing (use proper types)
ADR Reference: ADR-0005 (TypeScript), ADR-0015 (React 18), ADR-0016 (Next.js 14)
Styling
REQUIRED:
- Tailwind CSS 3.x (utility classes only)
- Shadcn UI components from @/components/ui/*
- Responsive design (mobile-first)
FORBIDDEN:
- CSS modules
- Styled-components
- Inline styles (except dynamic values)
- Custom CSS files (use Tailwind)
ADR Reference: ADR-0017 (Tailwind), ADR-0018 (Shadcn UI)
State Management
REQUIRED:
- Server state: TanStack Query (React Query)
- Global client state: Zustand
- Local component state: useState
FORBIDDEN:
- Redux, MobX, Recoil
- React Context for complex state
ADR Reference: ADR-0019 (Zustand), ADR-0020 (TanStack Query)
Forms
REQUIRED:
- React Hook Form
- Zod for validation
- zodResolver for integration
Example:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
const schema = z.object({
  name: z.string().min(1, "Required"),
});
const form = useForm({
  resolver: zodResolver(schema),
});
ADR Reference: ADR-0021 (React Hook Form + Zod)
Authentication Setup
Supabase Auth (Google OIDC)
Important: Supabase is used ONLY for authentication (Google OIDC), not for data queries. All data access goes through the Fastify API.
// lib/supabase/client.ts
import { createClientComponentClient } from '@supabase/ssr'
export const createClient = () => {
  return createClientComponentClient()
}
// lib/supabase/server.ts
import { createServerComponentClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
export const createClient = () => {
  const cookieStore = cookies()
  return createServerComponentClient({ cookies: () => cookieStore })
}
Usage: Authentication only (see Authentication Patterns)
NOT for data queries: Use API client instead (see API Integration)
Database Schema Reference
All tables follow this structure:
CREATE TABLE table_name (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  org_id UUID NOT NULL REFERENCES organizations(id),
  account_id UUID NOT NULL REFERENCES accounts(id),
  -- table-specific columns
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Row-Level Security
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON table_name
  USING (org_id = current_setting('app.current_org_id')::UUID);
Key tables for MVP.0:
| Table | Description | Columns | 
|---|---|---|
| organizations | Top-level tenant | name, settings | 
| accounts | Sub-tenant | org_id, name, type | 
| users | User accounts | email, role, name | 
| properties | Physical properties | name, address, amenities | 
| units | Bookable units | property_id, name, type | 
| channel_targets | Distribution channels | channel_type, config | 
| sync_jobs | Sync history | status, payload | 
Reference: See /docs/reference/database/migrations/ for complete schema
Authentication Patterns
Session Check
// app/dashboard/page.tsx
import { createClient } from '@/lib/supabase/server'
import { redirect } from 'next/navigation'
export default async function DashboardPage() {
  const supabase = createClient()
  const { data: { session } } = await supabase.auth.getSession()
  if (!session) {
    redirect('/login')
  }
  return <Dashboard user={session.user} />
}
Login Flow
// app/login/page.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useState } from 'react'
export default function LoginPage() {
  const supabase = createClient()
  const [loading, setLoading] = useState(false)
  const handleGoogleLogin = async () => {
    setLoading(true)
    const { error } = await supabase.auth.signInWithOAuth({
      provider: 'google',
      options: {
        redirectTo: `${window.location.origin}/auth/callback`
      }
    })
    if (error) {
      console.error('Login error:', error)
      setLoading(false)
    }
  }
  return (
    <Button onClick={handleGoogleLogin} disabled={loading}>
      {loading ? 'Signing in...' : 'Sign in with Google'}
    </Button>
  )
}
Auth Callback
// app/auth/callback/route.ts
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
export async function GET(request: Request) {
  const requestUrl = new URL(request.url)
  const code = requestUrl.searchParams.get('code')
  if (code) {
    const supabase = createClient()
    await supabase.auth.exchangeCodeForSession(code)
  }
  return NextResponse.redirect(requestUrl.origin + '/dashboard')
}
ADR Reference: ADR-0001 (Google OIDC + RBAC)
Multi-Tenancy Patterns
Organization Context (Zustand Store)
// lib/stores/auth-store.ts
import { create } from 'zustand'
interface AuthState {
  user: User | null
  currentOrgId: string | null
  currentAccountId: string | null
  setUser: (user: User) => void
  setCurrentOrg: (orgId: string, accountId: string) => void
}
export const useAuthStore = create<AuthState>((set) => ({
  user: null,
  currentOrgId: null,
  currentAccountId: null,
  setUser: (user) => set({ user }),
  setCurrentOrg: (orgId, accountId) => set({
    currentOrgId: orgId,
    currentAccountId: accountId
  }),
}))
Filtered Queries
// Every query MUST filter by org_id
'use client'
import { useAuthStore } from '@/lib/stores/auth-store'
import { useQuery } from '@tanstack/react-query'
export function useProperties() {
  const { currentOrgId } = useAuthStore()
  return useQuery({
    queryKey: ['properties', currentOrgId],
    queryFn: async () => {
      const supabase = createClient()
      const { data, error } = await supabase
        .from('properties')
        .select('*')
        .eq('org_id', currentOrgId)  // ← REQUIRED!
      if (error) throw error
      return data
    },
    enabled: !!currentOrgId,
  })
}
CRITICAL: Every database query MUST include .eq('org_id', currentOrgId) to enforce tenant isolation.
ADR Reference: ADR-0002 (Multi-Tenancy)
API Integration
✅ API-FIRST APPROACH
TVL Platform uses a Fastify REST API from day 1 for MVP.0, ensuring full ADR compliance and future-proofing for mobile apps (V2.0) and external partners.
Why API-first?
- ✅ Clear contract between frontend and backend (OpenAPI spec)
- ✅ Parallel development (frontend mocks API while backend implements)
- ✅ Mobile-ready from day 1 (no refactoring needed in V2.0)
- ✅ Better testing (mock API responses)
- ✅ No migration debt (no refactoring needed in MVP.1)
Reference: ADR-0054: API-First Approach for MVP.0
API Endpoints for MVP.0
All endpoints follow REST conventions with /api/v1/ prefix (ADR-0026):
Core Resources
- Spaces: GET|POST|PUT|PATCH|DELETE /api/v1/spaces
- Units: GET|POST|PUT|PATCH|DELETE /api/v1/units
- Channel Targets: GET|POST|PUT|DELETE /api/v1/channel-targets
- Channel Listings: GET|POST|PUT|DELETE /api/v1/channel-listings
Supporting APIs
- Organizations: GET /api/v1/organizations(read-only)
- Accounts: GET /api/v1/accounts(read-only)
- Sync: POST /api/v1/sync/trigger,GET /api/v1/sync/status/:id
- Auth: GET /api/v1/auth/session,POST /api/v1/auth/logout
Complete spec: ADR-0054 API Endpoints
API Client Pattern
// lib/api/client.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export class ApiClient {
  private async getAccessToken(): Promise<string> {
    // Get Supabase Auth JWT token
    const supabase = createClient()
    const { data: { session } } = await supabase.auth.getSession()
    if (!session) throw new Error('No session')
    return session.access_token
  }
  private async request<T>(
    endpoint: string,
    options?: RequestInit
  ): Promise<T> {
    const token = await this.getAccessToken()
    const response = await fetch(`${API_URL}${endpoint}`, {
      ...options,
      headers: {
        'Authorization': `Bearer ${token}`,
        'Content-Type': 'application/json',
        ...options?.headers,
      },
    })
    if (!response.ok) {
      // RFC 7807 Problem Details (ADR-0025)
      const problem = await response.json()
      throw new ApiError(problem)
    }
    return response.json()
  }
  // Spaces
  async getSpaces(orgId: string): Promise<Space[]> {
    const { spaces } = await this.request<{ spaces: Space[] }>(
      `/api/v1/spaces?org_id=${orgId}`
    )
    return spaces
  }
  async createSpace(data: CreateSpaceRequest): Promise<Space> {
    const { space } = await this.request<{ space: Space }>(
      '/api/v1/spaces',
      {
        method: 'POST',
        body: JSON.stringify(data),
      }
    )
    return space
  }
  async updateSpace(id: string, data: UpdateSpaceRequest): Promise<Space> {
    const { space } = await this.request<{ space: Space }>(
      `/api/v1/spaces/${id}`,
      {
        method: 'PUT',
        body: JSON.stringify(data),
      }
    )
    return space
  }
  async deleteSpace(id: string): Promise<void> {
    await this.request(`/api/v1/spaces/${id}`, { method: 'DELETE' })
  }
  // Units (same pattern)
  async getUnits(orgId: string): Promise<Unit[]> { /* ... */ }
  async createUnit(data: CreateUnitRequest): Promise<Unit> { /* ... */ }
  // Channel Targets (same pattern)
  async getChannelTargets(orgId: string): Promise<ChannelTarget[]> { /* ... */ }
  // Channel Listings (same pattern)
  async getChannelListings(orgId: string): Promise<ChannelListing[]> { /* ... */ }
}
export const apiClient = new ApiClient()
TanStack Query Hooks
// hooks/use-spaces.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { apiClient } from '@/lib/api/client'
import { useAuthStore } from '@/lib/stores/auth-store'
export function useSpaces() {
  const { currentOrgId } = useAuthStore()
  return useQuery({
    queryKey: ['spaces', currentOrgId],
    queryFn: () => apiClient.getSpaces(currentOrgId!),
    enabled: !!currentOrgId,
  })
}
export function useCreateSpace() {
  const queryClient = useQueryClient()
  const { currentOrgId, currentAccountId } = useAuthStore()
  return useMutation({
    mutationFn: (data: Omit<CreateSpaceRequest, 'org_id' | 'account_id'>) =>
      apiClient.createSpace({
        ...data,
        org_id: currentOrgId!,
        account_id: currentAccountId!,
      }),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['spaces', currentOrgId] })
    },
  })
}
export function useUpdateSpace() {
  const queryClient = useQueryClient()
  const { currentOrgId } = useAuthStore()
  return useMutation({
    mutationFn: ({ id, data }: { id: string; data: UpdateSpaceRequest }) =>
      apiClient.updateSpace(id, data),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['spaces', currentOrgId] })
    },
  })
}
export function useDeleteSpace() {
  const queryClient = useQueryClient()
  const { currentOrgId } = useAuthStore()
  return useMutation({
    mutationFn: (id: string) => apiClient.deleteSpace(id),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['spaces', currentOrgId] })
    },
  })
}
Pattern: Same hooks for units, channel targets, and channel listings.
Benefits of API-First Approach
| Aspect | API-First (ADR-0054) | Supabase Direct (Rejected) | 
|---|---|---|
| Timeline | 4 weeks | 4 weeks (same) | 
| Mobile apps | ✅ Ready for V2.0 | ❌ Requires migration | 
| External partners | ✅ Can integrate now | ❌ Blocked | 
| Frontend testing | ✅ Mock API | ⚠️ Requires database | 
| Migration effort | ✅ Zero | 🔴 High (MVP.1 refactor) | 
| Type safety | ✅ OpenAPI types | ⚠️ Supabase types | 
| ADR compliance | ✅ Full compliance | ❌ Violates ADRs 0022-0026 | 
Error Handling
// components/property-list.tsx
'use client'
import { useProperties } from '@/hooks/use-properties'
export default function PropertyList() {
  const { data: properties, isLoading, isError, error } = useProperties()
  if (isLoading) {
    return <LoadingSpinner />
  }
  if (isError) {
    return (
      <Alert variant="destructive">
        <AlertTitle>Error</AlertTitle>
        <AlertDescription>
          {error.message || 'Failed to load properties'}
        </AlertDescription>
      </Alert>
    )
  }
  return (
    <div>
      {properties.map(property => (
        <PropertyCard key={property.id} property={property} />
      ))}
    </div>
  )
}
ADR Reference: ADR-0020 (TanStack Query), ADR-0025 (RFC 7807 Error Handling)
File Structure
app/
├── (auth)/
│   ├── login/
│   │   └── page.tsx           # Login page
│   └── auth/
│       └── callback/
│           └── route.ts        # OAuth callback handler
│
├── (dashboard)/
│   ├── layout.tsx              # Dashboard layout (nav, sidebar)
│   ├── dashboard/
│   │   └── page.tsx            # Dashboard home
│   ├── properties/
│   │   ├── page.tsx            # Property list
│   │   ├── [id]/
│   │   │   └── page.tsx        # Property detail
│   │   └── new/
│   │       └── page.tsx        # Create property
│   └── bookings/
│       └── page.tsx            # Booking list
│
components/
├── ui/                         # Shadcn UI components (generated)
│   ├── button.tsx
│   ├── input.tsx
│   ├── table.tsx
│   └── ...
└── features/                   # Feature-specific components
    ├── properties/
    │   ├── property-list.tsx
    │   ├── property-card.tsx
    │   └── property-form.tsx
    └── bookings/
        └── booking-calendar.tsx
hooks/
├── use-properties.ts
├── use-bookings.ts
└── use-auth.ts
lib/
├── supabase/
│   ├── client.ts               # Client-side Supabase
│   └── server.ts               # Server-side Supabase
└── stores/
    └── auth-store.ts           # Zustand auth store
types/
├── database.ts                 # Generated from Supabase
└── index.ts                    # Shared types
Code Patterns
Component Pattern
// components/features/properties/property-card.tsx
'use client'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import type { Property } from '@/types'
interface PropertyCardProps {
  property: Property
  onEdit?: (property: Property) => void
  onDelete?: (id: string) => void
}
export function PropertyCard({ property, onEdit, onDelete }: PropertyCardProps) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{property.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p className="text-sm text-muted-foreground">
          {property.address}
        </p>
        <div className="mt-4 flex gap-2">
          <Button
            size="sm"
            onClick={() => onEdit?.(property)}
          >
            Edit
          </Button>
          <Button
            size="sm"
            variant="destructive"
            onClick={() => onDelete?.(property.id)}
          >
            Delete
          </Button>
        </div>
      </CardContent>
    </Card>
  )
}
Page Pattern (Server Component)
// app/(dashboard)/properties/page.tsx
import { createClient } from '@/lib/supabase/server'
import { PropertyList } from '@/components/features/properties/property-list'
export default async function PropertiesPage() {
  const supabase = createClient()
  // Fetch initial data on server
  const { data: properties } = await supabase
    .from('properties')
    .select('*')
  return (
    <div className="container mx-auto py-8">
      <h1 className="text-3xl font-bold mb-6">Properties</h1>
      <PropertyList initialData={properties} />
    </div>
  )
}
Client Component with Data Fetching
// components/features/properties/property-list.tsx
'use client'
import { useProperties } from '@/hooks/use-properties'
import { PropertyCard } from './property-card'
import type { Property } from '@/types'
interface PropertyListProps {
  initialData?: Property[]
}
export function PropertyList({ initialData }: PropertyListProps) {
  const { data: properties, isLoading } = useProperties({
    initialData,
  })
  if (isLoading) {
    return <div>Loading...</div>
  }
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
      {properties?.map(property => (
        <PropertyCard key={property.id} property={property} />
      ))}
    </div>
  )
}
Standard Prompts
Prompt 1: Data Table Page
Create a properties list page for a vacation rental management platform.
Requirements:
- Next.js 14 App Router
- Fetch data from API endpoint: GET /api/v1/spaces
- Response format: { "spaces": [{ id, org_id, name, address, status, ... }] }
- Multi-tenant: API filters by org_id automatically
- Use TanStack Query for data fetching
- Display in Shadcn UI Table
- Columns: Property Name, Address, Units Count, Status, Actions
- Filters: Search by name, Status dropdown (Active/Inactive)
- Pagination: 10 rows per page
- Actions: Edit (pencil icon), Delete (trash icon)
- Responsive: Switch to cards on mobile
- TypeScript with strict types
API Client:
- Import from '@/lib/api/client'
- Methods: apiClient.getSpaces(orgId), apiClient.deleteSpace(id)
- Auth: Automatic (JWT token included in apiClient)
Use React Hook Form + Zod if filters are in a form.
Prompt 2: Create Form
Create a property creation form for a vacation rental platform.
Requirements:
- Next.js 14 App Router
- Multi-step form (3 steps: Basic Info, Location, Details)
- React Hook Form with Zod validation
- Shadcn UI Form components
- TypeScript
Fields:
Step 1 - Basic Info:
- Property Name (required, min 3 chars)
- Description (required, min 10 chars)
Step 2 - Location:
- Address (required)
- City (required)
- State (dropdown, US states)
- Zip Code (required, 5 digits)
Step 3 - Details:
- Property Type (dropdown: House, Apartment, Villa)
- Units Count (number, min 1)
On submit:
- POST to API endpoint: /api/v1/spaces
- Request body: { name, description, address, city, state, zip, property_type, units_count }
- org_id and account_id added automatically by API from JWT token
- Use apiClient.createSpace() from '@/lib/api/client'
- Show success toast (Shadcn Toast)
- Redirect to properties list
Validation errors should display inline.
Show stepper at top to indicate progress.
Error handling: Display RFC 7807 error details if API returns error.
Prompt 3: Dashboard Page
Create a dashboard home page for a vacation rental management platform.
Requirements:
- Next.js 14 App Router
- Client Component with TanStack Query
- Multi-tenant: API filters by org_id automatically
API Endpoints:
- GET /api/v1/spaces (for properties count)
- GET /api/v1/channel-listings (for active listings)
- Use apiClient from '@/lib/api/client'
Layout:
- 4 metric cards at top (Total Properties, Active Listings, Sync Success Rate, Last Sync)
- Recent sync operations table below (5 rows)
- Quick actions section (buttons to add property, add unit, trigger sync)
Metric Cards:
- Use Shadcn Card component
- Show value and trend (e.g., +12% from last week)
- Icons for each metric
- Click to navigate to detail pages
Recent Sync Operations Table:
- Columns: Unit Name, Channel, Status, Last Synced, Actions
- Status badges (Completed = green, Pending = yellow, Failed = red)
- Actions: Retry button (for failed syncs)
Data Fetching:
- Use TanStack Query hooks (useSpaces, useChannelListings)
- Show loading skeleton while fetching
- Error states with retry button
Responsive: Stack cards on mobile (grid-cols-1 sm:grid-cols-2 lg:grid-cols-4)
TypeScript with strict types
Prompt 4: Modal/Dialog
Create a booking details modal for a vacation rental platform.
Requirements:
- Shadcn UI Dialog component
- Opens when clicking "View Details" in bookings table
- TypeScript
Content:
- Booking ID
- Guest Name
- Property Name
- Check-in Date
- Check-out Date
- Total Price
- Status (badge: Confirmed, Pending, Cancelled)
- Created At
Actions:
- Edit Booking (button)
- Cancel Booking (button with confirmation dialog)
- Close (X button in top-right)
Props:
interface BookingDetailsModalProps {
  booking: Booking | null
  open: boolean
  onOpenChange: (open: boolean) => void
  onEdit?: (booking: Booking) => void
  onCancel?: (bookingId: string) => void
}
Use Shadcn Dialog, Badge, Button components
Responsive design
Environment Variables
Required Variables
# .env.local
NEXT_PUBLIC_SUPABASE_URL=https://xxxxx.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJxxx...
# Server-only (never expose to client)
SUPABASE_SERVICE_ROLE_KEY=eyJxxx...
Vercel Deployment
When deploying to Vercel:
- Install Supabase integration (automatically configures env vars)
- Environment variables sync from Supabase dashboard
- Auth redirect URLs auto-update for preview deployments
Development Workflow
Phase 1: Schema Design (Week 1)
1. Design database schema (migrations written)
2. Create types from schema
3. Setup Supabase project
4. Run migrations
Phase 2: UI Components (Week 2-3) - Use v0.dev
1. Generate dashboard layout with v0.dev
2. Generate property list page with v0.dev
3. Generate property form with v0.dev
4. Generate booking calendar with v0.dev
For each component:
- Feed this specification into prompts
- Review generated code
- Replace mock data with Supabase queries
- Add multi-tenancy filters (org_id)
- Test locally
- Commit
Phase 3: API Integration (Week 3-4)
1. Replace all mock data with TanStack Query
2. Add Zustand auth store
3. Implement authentication flow
4. Test multi-tenancy isolation
5. Add error handling
Phase 4: Deploy (Week 4)
1. Deploy to Vercel
2. Connect Supabase integration
3. Test in staging
4. Deploy to production
Parallel Development Strategy
✅ You CAN Work in Parallel:
Backend Team:
- Week 1: Design schema, write migrations
- Week 2: Create Supabase tables, enable RLS
- Week 3: Test RLS policies, seed data
- Week 4: Setup production database
Frontend Team:
- Week 1: Setup Next.js project, Shadcn UI
- Week 2: Generate UI with v0.dev (using this spec)
- Week 3: Integrate TanStack Query, connect to Supabase
- Week 4: End-to-end testing, deploy
Dependencies:
- Frontend needs database schema design (Week 1) - NOT complete database
- Frontend can use types generated from schema
- Frontend can start with mock data, replace with real API later
Result: 4-week timeline instead of 8 weeks sequential
Integration Checklist
Before integrating v0.dev components into your project:
- Database schema designed (types available)
- Supabase project created
- Environment variables configured
- Auth store (Zustand) created
- Supabase client/server helpers created
- Multi-tenancy pattern understood
- TanStack Query provider configured
- This specification reviewed
After generating components with v0.dev:
- Code reviewed against ADRs
- Mock data replaced with Supabase queries
- Multi-tenancy filter added (org_id)
- Loading states added
- Error handling added
- TypeScript strict mode passing
- Responsive on mobile tested
- Accessibility checked (keyboard nav, screen reader)
FAQ
Q: Can I start frontend before database is complete?
A: Yes! Frontend needs schema design (types) but not complete database. Use v0.dev to generate UI with mock data, then integrate Supabase as backend becomes available.
Q: How do I get TypeScript types from Supabase?
A: Generate types from Supabase CLI:
npx supabase gen types typescript --project-id xxxxx > types/database.ts
Q: Should every v0.dev prompt include this entire spec?
A: No, only include relevant sections. For example:
- Creating a form? Include "Forms" and "Multi-Tenancy" sections
- Creating a table? Include "TanStack Query" and "Database Connection" sections
Q: What if v0.dev doesn't follow these constraints?
A: Iterate in v0.dev's chat interface. Paste constraint, ask to regenerate. If still wrong, manually fix after export.
Q: Can v0.dev generate the entire admin dashboard?
A: No. Generate one screen at a time for better results:
- Dashboard page
- Property list page
- Property form page
- Booking calendar page
Then integrate them.
Summary
This specification ensures:
- ✅ v0.dev output aligns with ADR-0005 through ADR-0021
- ✅ Multi-tenancy patterns enforced (org_id filtering)
- ✅ Supabase integration follows best practices
- ✅ Frontend and backend can develop in parallel
- ✅ Code is production-ready with minor customizations
Usage:
- Before prompting v0.dev: Review this spec, copy relevant sections
- When prompting: Paste constraints into v0.dev chat
- After generating: Review code against checklist
- Before committing: Test multi-tenancy, auth, error handling
Timeline: With this approach, MVP.0 UI can be built in 2-3 weeks (vs 5-6 weeks from scratch).
Related Documentation:
- v0.dev Usage Guide - How to use v0.dev effectively
- ADR Index - All architectural decisions
- Database Migrations - Complete schema
- Multi-Tenancy Guide - RLS patterns
Last Updated: 2025-01-26 Maintained By: Frontend Team Status: Active for MVP.0