Skip to main content

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:

  1. No clear API contract - Frontend and backend coupled through database schema
  2. Mobile apps blocked - Cannot reuse frontend code for mobile (V2.0)
  3. External partners blocked - No API for future integrations
  4. Refactoring burden - Large migration effort in MVP.1 to add API layer
  5. 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:

  1. Clear Contract - OpenAPI spec defines frontend/backend interface
  2. Parallel Development - Frontend can mock API while backend implements
  3. Future-Proof - Mobile apps (V2.0) and partners can use same API
  4. No Migration Debt - No refactoring needed in MVP.1
  5. 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)

AspectSupabase Direct (ADR-0053)API-First (ADR-0054)
Timeline4 weeks4 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

Fully Compliant:

Supersedes:


References


Last Updated: 2025-01-26 Status: Active - API-first approach for MVP.0 Review Date: End of Week 2 (validate API implementation progress)