ADR-0054: API-First Approach for MVP.0
Status
Accepted - 2025-01-26
Supersedes: ADR-0053 (Rejected)
Context
During MVP.0 planning, we briefly considered using Supabase direct access from the frontend to ship faster (ADR-0053). However, this approach has critical flaws:
- No clear API contract - Frontend and backend coupled through database schema
- Mobile apps blocked - Cannot reuse frontend code for mobile (V2.0)
- External partners blocked - No API for future integrations
- Refactoring burden - Large migration effort in MVP.1 to add API layer
- Testing complexity - Cannot test frontend without database
The Question: What is the minimum API surface to support MVP.0 without delaying the timeline?
Decision
Build a minimal Fastify API from day 1 with only the endpoints needed for MVP.0 admin UI.
Rationale
Why API-First Wins:
- Clear Contract - OpenAPI spec defines frontend/backend interface
- Parallel Development - Frontend can mock API while backend implements
- Future-Proof - Mobile apps (V2.0) and partners can use same API
- No Migration Debt - No refactoring needed in MVP.1
- Better Testing - Frontend tests mock API, backend tests validate endpoints
Why Minimal API Works:
- MVP.0 only needs 4 core resources (spaces, units, channel targets, channel listings)
- Standard REST CRUD pattern (no complex logic)
- Can implement in 1-2 weeks (not 3 weeks as originally estimated)
- OpenAPI spec generated automatically from Fastify schemas
Minimum API Endpoints for MVP.0
Core Resources (CRUD)
1. Spaces (Properties)
GET    /api/v1/spaces                  # List all spaces for org
GET    /api/v1/spaces/:id              # Get space details
POST   /api/v1/spaces                  # Create space
PUT    /api/v1/spaces/:id              # Update space (full)
PATCH  /api/v1/spaces/:id              # Update space (partial)
DELETE /api/v1/spaces/:id              # Delete space
Fields: id, org_id, account_id, name, address, city, state, zip, country, amenities, description, status, created_at, updated_at
2. Units (Bookable Inventory)
GET    /api/v1/units                   # List all units for org
GET    /api/v1/units/:id               # Get unit details
POST   /api/v1/units                   # Create unit
PUT    /api/v1/units/:id               # Update unit (full)
PATCH  /api/v1/units/:id               # Update unit (partial)
DELETE /api/v1/units/:id               # Delete unit
Fields: id, org_id, account_id, space_id, name, type, bedrooms, bathrooms, max_guests, base_price_cents, status, created_at, updated_at
Includes: space (nested object for display)
3. Channel Targets (Hostaway Connection)
GET    /api/v1/channel-targets         # List all channel targets for org
GET    /api/v1/channel-targets/:id     # Get channel target details
POST   /api/v1/channel-targets         # Create channel target
PUT    /api/v1/channel-targets/:id     # Update channel target
DELETE /api/v1/channel-targets/:id     # Delete channel target
Fields: id, org_id, account_id, channel_type (hostaway), name, config (JSONB), status, last_health_check_at, created_at, updated_at
4. Channel Listings (Unit → Channel Mapping)
GET    /api/v1/channel-listings        # List all channel listings for org
GET    /api/v1/channel-listings/:id    # Get channel listing details
POST   /api/v1/channel-listings        # Create channel listing (link unit to channel)
PUT    /api/v1/channel-listings/:id    # Update channel listing
DELETE /api/v1/channel-listings/:id    # Delete channel listing (unlink)
Fields: id, org_id, account_id, unit_id, channel_target_id, external_id, status, last_synced_at, error_message, created_at, updated_at
Includes: unit, channel_target (nested objects for display)
Supporting APIs
5. Organizations (Read-Only for MVP.0)
GET    /api/v1/organizations           # List user's organizations
GET    /api/v1/organizations/:id       # Get organization details
Fields: id, name, settings, created_at, updated_at
Note: Create/update/delete deferred to MVP.1 (users provisioned manually)
6. Accounts (Read-Only for MVP.0)
GET    /api/v1/accounts                # List accounts for current org
GET    /api/v1/accounts/:id            # Get account details
Fields: id, org_id, name, type, created_at, updated_at
Operational APIs
7. Sync Operations
POST   /api/v1/sync/trigger            # Manually trigger sync for a listing
GET    /api/v1/sync/status/:listingId  # Get sync status and history
GET    /api/v1/sync/audit              # Get sync audit log (paginated)
Trigger Request:
{
  "channel_listing_id": "uuid"
}
Status Response:
{
  "channel_listing_id": "uuid",
  "status": "pending" | "in_progress" | "completed" | "failed",
  "last_synced_at": "2025-01-26T12:00:00Z",
  "error_message": null,
  "attempts": 0
}
Authentication API
8. Session Management
GET    /api/v1/auth/session            # Get current session (user, org, account, role)
POST   /api/v1/auth/logout             # Logout (clear session)
Session Response:
{
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "name": "John Doe"
  },
  "organization": {
    "id": "uuid",
    "name": "Acme Properties"
  },
  "account": {
    "id": "uuid",
    "name": "Primary Account"
  },
  "role": "owner",
  "permissions": ["spaces:read", "spaces:write", ...]
}
Note: Login handled by Supabase Auth (Google OIDC), API validates JWT token.
API Architecture
Stack
- Framework: Fastify 4.x (ADR-0022)
- Validation: JSON Schema (built into Fastify)
- ORM: Drizzle (ADR-0012)
- Auth: JWT validation (Supabase Auth tokens)
- Docs: OpenAPI 3.0 auto-generated (ADR-0024)
- Errors: RFC 7807 Problem Details (ADR-0025)
- Versioning: /api/v1/prefix (ADR-0026)
Folder Structure
services/api/
├── src/
│   ├── routes/
│   │   ├── v1/
│   │   │   ├── spaces.ts         # Space CRUD
│   │   │   ├── units.ts          # Unit CRUD
│   │   │   ├── channel-targets.ts
│   │   │   ├── channel-listings.ts
│   │   │   ├── organizations.ts
│   │   │   ├── accounts.ts
│   │   │   ├── sync.ts
│   │   │   └── auth.ts
│   ├── services/
│   │   ├── spaces.service.ts     # Business logic
│   │   ├── units.service.ts
│   │   └── sync.service.ts
│   ├── middleware/
│   │   ├── auth.ts               # JWT validation
│   │   ├── org-filter.ts         # Multi-tenancy filter
│   │   └── error-handler.ts      # RFC 7807 errors
│   ├── schemas/
│   │   ├── space.schema.ts       # JSON Schema for validation
│   │   └── unit.schema.ts
│   └── server.ts
├── package.json
└── tsconfig.json
Timeline Impact
Original Estimate (Supabase Direct):
- Week 1-2: Database schema + Supabase setup
- Week 3-4: Frontend with v0.dev (Supabase direct)
- Total: 4 weeks
API-First Estimate:
- Week 1: Database schema + Fastify API setup
- Week 2: Implement 8 API endpoints + OpenAPI spec
- Week 3-4: Frontend with v0.dev (API calls)
- Total: 4 weeks (same timeline)
Why no delay?
- API endpoints are simple CRUD (no complex business logic)
- OpenAPI spec auto-generated (no manual docs)
- Frontend can start Week 2 with mock API responses
Frontend Integration
v0.dev Prompts
When prompting v0.dev, specify API endpoints instead of Supabase:
Create a properties list page for a vacation rental platform.
API Integration:
- Fetch from API endpoint: GET /api/v1/spaces
- Response format: { "spaces": [...] }
- Use TanStack Query for data fetching
- Auth: Include Authorization header with JWT token
Display in Shadcn UI Table with columns:
- Property Name, Address, Units Count, Status, Actions
TypeScript with strict types.
API Client Pattern
// lib/api/client.ts
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'
export class ApiClient {
  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) {
      const problem = await response.json() // RFC 7807
      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
  }
  // Units, Channel Targets, etc. (same pattern)
}
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 } = useAuthStore()
  return useMutation({
    mutationFn: (space: CreateSpaceRequest) => apiClient.createSpace(space),
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['spaces', currentOrgId] })
    },
  })
}
Parallel Development Strategy
Week 1: Foundation
Backend:
-  Setup Fastify project (services/api/)
- Configure Drizzle ORM + database connection
- Setup JWT authentication middleware
- Setup OpenAPI plugin
- Setup RFC 7807 error handler
-  Create health check endpoint (GET /health)
Frontend:
- Setup Next.js 14 project
- Install Shadcn UI
-  Create API client (lib/api/client.ts)
- Create Zustand auth store
- Create TanStack Query provider
Database:
- Run migrations (spaces, units, channel_targets, channel_listings)
- Enable RLS policies
- Seed test data
Week 2: Core CRUD APIs
Backend:
-  Implement /api/v1/spaces(full CRUD)
-  Implement /api/v1/units(full CRUD)
-  Implement /api/v1/channel-targets(full CRUD)
-  Implement /api/v1/channel-listings(full CRUD)
- Integration tests for all endpoints
- Publish OpenAPI spec
Frontend:
-  Create TanStack Query hooks (use-spaces,use-units)
- Mock API responses for development
- Build UI with v0.dev (using API patterns)
Week 3: Supporting APIs + UI
Backend:
-  Implement /api/v1/organizations(read-only)
-  Implement /api/v1/accounts(read-only)
-  Implement /api/v1/sync/*endpoints
-  Implement /api/v1/auth/session
Frontend:
- Connect UI to real API endpoints
- Replace mocks with actual API calls
- Add error handling (RFC 7807)
- Add loading states
Week 4: Sync Engine + Testing
Backend:
- Implement sync job queue (BullMQ)
- Implement Hostaway connector
- Test one-way sync (TVL → Hostaway)
Frontend:
- Sync status dashboard
- Manual retry UI
- End-to-end testing
Success Criteria
MVP.0 Completion (Week 4):
- ✅ All CRUD operations via /api/v1/*endpoints
- ✅ OpenAPI spec published and accessible
- ✅ Frontend has zero direct database calls
- ✅ One-way sync (TVL → Hostaway) working via API
- ✅ 10+ test properties syncing successfully
Quality Gates:
- ✅ API response time <500ms (p95)
- ✅ OpenAPI validation passing (all requests match spec)
- ✅ RFC 7807 errors returned consistently
- ✅ Multi-tenancy isolation tested (org_id filtering)
Benefits Over Supabase Direct (ADR-0053)
| Aspect | Supabase Direct (ADR-0053) | API-First (ADR-0054) | 
|---|---|---|
| Timeline | 4 weeks | 4 weeks (same) | 
| Mobile apps | ❌ Blocked until MVP.1 | ✅ Ready for V2.0 | 
| External partners | ❌ Blocked until MVP.1 | ✅ Can integrate now | 
| Frontend testing | ⚠️ Requires database | ✅ Mock API | 
| Migration effort | 🔴 High (MVP.1 refactor) | ✅ Zero | 
| Type safety | ⚠️ Supabase types | ✅ OpenAPI types | 
| ADR compliance | ❌ Violates ADR-0022-0026 | ✅ Full compliance | 
Related ADRs
Fully Compliant:
- ADR-0001: Google OIDC + RBAC - Authentication
- ADR-0002: PostgreSQL Data Modeling - Database schema
- ADR-0022: Fastify API Framework - API framework
- ADR-0023: REST API Pattern - API design
- ADR-0024: OpenAPI Specification - API docs
- ADR-0025: RFC 7807 Error Handling - Error format
- ADR-0026: URL-Based API Versioning - API versioning
Supersedes:
- ADR-0053: Temporary Supabase Direct Access - Rejected
References
- MVP.0 Specification
- v0-project-spec.md - Updated for API-first
- Fastify Documentation
- OpenAPI 3.0 Specification
- RFC 7807: Problem Details
Last Updated: 2025-01-26 Status: Active - API-first approach for MVP.0 Review Date: End of Week 2 (validate API implementation progress)