Skip to main content

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

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:

TableDescriptionColumns
organizationsTop-level tenantname, settings
accountsSub-tenantorg_id, name, type
usersUser accountsemail, role, name
propertiesPhysical propertiesname, address, amenities
unitsBookable unitsproperty_id, name, type
channel_targetsDistribution channelschannel_type, config
sync_jobsSync historystatus, 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

AspectAPI-First (ADR-0054)Supabase Direct (Rejected)
Timeline4 weeks4 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:

  1. Install Supabase integration (automatically configures env vars)
  2. Environment variables sync from Supabase dashboard
  3. 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:

  1. Dashboard page
  2. Property list page
  3. Property form page
  4. 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:

  1. Before prompting v0.dev: Review this spec, copy relevant sections
  2. When prompting: Paste constraints into v0.dev chat
  3. After generating: Review code against checklist
  4. 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:


Last Updated: 2025-01-26 Maintained By: Frontend Team Status: Active for MVP.0