seatkit

SeatKit - Swift to TypeScript Migration Strategy

Migration Approach: Parallel Development (Swift app continues in production) Goal: Modern TypeScript web application inspired by proven Swift architecture Timeline: Foundation β†’ Core β†’ Features β†’ Polish Last Updated: 2025-10-25


🎯 Migration Overview

Philosophy: Inspired Rewrite, Not Direct Port

This is NOT a line-by-line translation from Swift to TypeScript. Instead, we’re creating a modern web application inspired by the proven Swift architecture, taking the best patterns and adapting them to web technologies.

Why Rewrite Instead of Port?


πŸ“Š Swift App Analysis Summary

Architecture Strengths to Preserve

βœ… Type Safety: Swift’s strong typing β†’ TypeScript strict mode + Zod βœ… Clean Architecture: MVVM + Services β†’ Similar pattern in TS βœ… Real-Time Sync: Firestore listeners β†’ WebSocket/SSE implementation βœ… Offline-First: SQLite + sync β†’ Consider similar approach βœ… Domain Modeling: Rich enums & entities β†’ Zod schemas + TS types

Patterns to Adapt

πŸ”„ Observable Pattern: @Published β†’ Zustand/Jotai state management πŸ”„ Dependency Injection: AppDependencies β†’ Context providers/DI container πŸ”„ Service Layer: Protocol-based β†’ Interface-based TypeScript πŸ”„ Repository Pattern: FirestoreDataStoreProtocol β†’ Repository interfaces πŸ”„ Actor Concurrency: Swift actors β†’ Web Workers or async patterns

Complexity to Simplify

❌ Over-Complex Layout: Simplify table layout visualization ❌ Apple-Specific: Drop Pencil drawing, native iOS features ❌ Tight Coupling: Decouple from Koenji-specific assumptions ❌ Manual Caching: Use modern caching strategies (TanStack Query)


πŸ— Migration Phases

Phase 1: Foundation βœ… COMPLETE

Status: Architectural decisions made, documented

Completed:

Outcome: Solid foundation for development

Phase 2: Core Architecture 🎯 NEXT

Goal: Make architectural decisions that enable everything else

Database Strategy:

API Architecture:

Frontend Framework:

Phase 3: Domain Model Implementation

Goal: Port core business entities and validation

Tasks:

Success Criteria:

Phase 4: Data Layer & Services

Goal: Implement data persistence and business services

Tasks:

Migration Strategy:

// Swift Pattern
class ReservationService: ObservableObject {
    @Published var reservations: [Reservation] = []
    private let store: FirestoreDataStoreProtocol
}

// TypeScript Equivalent
class ReservationService {
    private reservationRepo: ReservationRepository;

    async getReservations(date: string): Promise<Reservation[]> {
        return await this.reservationRepo.findByDate(date);
    }
}

Phase 5: User Interface

Goal: Create responsive web interface matching Swift app functionality

Timeline/List Views (Priority 1):

Table Layout View (Priority 2):

Sales Interface (Priority 3):

Phase 6: Real-Time Collaboration

Goal: Multi-user editing with conflict resolution

Tasks:

Phase 7: Configuration & Multi-Restaurant

Goal: Make system configurable for different restaurants

Tasks:

Phase 8: Polish & Performance

Goal: Production-ready application

Tasks:


πŸ”„ Entity Migration Mapping

Reservation Entity Migration

Swift Model (simplified):

struct Reservation: Codable, Identifiable {
    let id: String
    let name: String
    let phone: String
    let numberOfPersons: Int
    let dateString: String
    let startTime: String
    var endTime: String?

    let category: ReservationCategory
    let type: ReservationType
    var status: ReservationStatus
    var acceptance: Acceptance

    // Enums
    enum ReservationCategory: String, CaseIterable {
        case lunch, dinner, noBookingZone
    }

    enum ReservationStatus: String, CaseIterable {
        case pending, confirmed, canceled, noShow, showedUp, late, toHandle, deleted, na
    }

    // ... more properties
}

TypeScript Migration:

// @seatkit/types/src/reservation.ts
import { z } from 'zod';

export const ReservationCategorySchema = z.enum([
	'lunch',
	'dinner',
	'noBookingZone',
]);
export const ReservationStatusSchema = z.enum([
	'pending',
	'confirmed',
	'canceled',
	'noShow',
	'showedUp',
	'late',
	'toHandle',
	'deleted',
]);
export const ReservationTypeSchema = z.enum([
	'walkIn',
	'inAdvance',
	'waitingList',
]);
export const AcceptanceSchema = z.enum(['confirmed', 'toConfirm', 'na']);

export const ReservationSchema = z.object({
	id: z.string().uuid(),
	name: z.string().min(1),
	phone: z.string().min(1),
	numberOfPersons: z.number().int().min(1).max(20),
	dateString: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
	startTime: z.string().regex(/^\d{2}:\d{2}$/),
	endTime: z
		.string()
		.regex(/^\d{2}:\d{2}$/)
		.optional(),

	category: ReservationCategorySchema,
	type: ReservationTypeSchema,
	status: ReservationStatusSchema,
	acceptance: AcceptanceSchema,

	// Optional fields
	specialRequests: z.string().optional(),
	dietaryRestrictions: z.string().optional(),
	language: z.enum(['italian', 'english', 'japanese']).optional(),
	colorHue: z.number().min(0).max(360).optional(),

	// Metadata
	createdAt: z.date(),
	lastEdited: z.date(),
	editedBy: z.string().optional(),
});

export type Reservation = z.infer<typeof ReservationSchema>;
export type ReservationCategory = z.infer<typeof ReservationCategorySchema>;
export type ReservationStatus = z.infer<typeof ReservationStatusSchema>;
// ... other exported types

Service Layer Migration

Swift Service Pattern:

protocol FirestoreDataStoreProtocol {
    func insert<T: Codable>(_ item: T) async throws
    func update<T: Codable>(_ item: T) async throws
    func delete<T: Codable>(_ item: T) async throws
    func streamReservations() -> AsyncThrowingStream<[Reservation], Error>
}

class ReservationService: ObservableObject {
    @Published var reservations: [Reservation] = []
    private let dataStore: FirestoreDataStoreProtocol

    func addReservation(_ reservation: Reservation) async {
        do {
            try await dataStore.insert(reservation)
        } catch {
            // Handle error
        }
    }
}

TypeScript Service Pattern:

// Repository interface (equivalent to Swift protocol)
interface ReservationRepository {
	insert(reservation: Reservation): Promise<void>;
	update(reservation: Reservation): Promise<void>;
	delete(id: string): Promise<void>;
	findByDate(date: string): Promise<Reservation[]>;
	findById(id: string): Promise<Reservation | null>;
	streamReservations(
		date: string,
	): AsyncGenerator<Reservation[], void, unknown>;
}

// Service implementation
class ReservationService {
	constructor(private repo: ReservationRepository) {}

	async addReservation(data: unknown): Promise<Reservation> {
		// Validate with Zod
		const reservation = ReservationSchema.parse(data);

		// Business logic
		await this.validateTimeSlot(reservation);

		// Persist
		await this.repo.insert(reservation);

		return reservation;
	}

	private async validateTimeSlot(reservation: Reservation): Promise<void> {
		// Business rule validation
		const conflicts = await this.repo.findConflicts(
			reservation.dateString,
			reservation.startTime,
			reservation.endTime,
		);

		if (conflicts.length > 0) {
			throw new Error('Time slot conflict');
		}
	}
}

πŸ“± User Interface Migration

View Architecture Migration

Swift Pattern (SwiftUI):

struct ReservationListView: View {
    @ObservedObject var viewModel: ReservationService
    @State private var showingAddReservation = false

    var body: some View {
        List(viewModel.reservations) { reservation in
            ReservationRow(reservation: reservation)
        }
        .onAppear {
            viewModel.loadReservations()
        }
    }
}

TypeScript/React Pattern:

// Component with hooks
function ReservationListView() {
  const { data: reservations, isLoading } = useReservations();
  const [showingAddReservation, setShowingAddReservation] = useState(false);

  return (
    <div className="space-y-4">
      {reservations?.map(reservation => (
        <ReservationRow
          key={reservation.id}
          reservation={reservation}
        />
      ))}
    </div>
  );
}

// Data fetching hook
function useReservations() {
  return useQuery({
    queryKey: ['reservations', selectedDate],
    queryFn: () => reservationService.getReservations(selectedDate),
    refetchInterval: 5000, // Real-time updates
  });
}

State Management Migration

Swift Observable Pattern:

class ReservationStore: ObservableObject {
    @Published var reservations: [Reservation] = []
    @Published var selectedDate = Date()
    @Published var isLoading = false
}

TypeScript State Management (using Zustand):

interface ReservationStore {
	reservations: Reservation[];
	selectedDate: string;
	isLoading: boolean;

	setReservations: (reservations: Reservation[]) => void;
	setSelectedDate: (date: string) => void;
	setLoading: (loading: boolean) => void;
}

const useReservationStore = create<ReservationStore>(set => ({
	reservations: [],
	selectedDate: new Date().toISOString().split('T')[0],
	isLoading: false,

	setReservations: reservations => set({ reservations }),
	setSelectedDate: selectedDate => set({ selectedDate }),
	setLoading: isLoading => set({ isLoading }),
}));

πŸ”„ Real-Time Sync Migration

Swift Pattern (Firestore Listeners):

func streamReservations() -> AsyncThrowingStream<[Reservation], Error> {
    AsyncThrowingStream { continuation in
        let listener = db.collection("reservations")
            .addSnapshotListener { querySnapshot, error in
                if let error = error {
                    continuation.finish(throwing: error)
                    return
                }

                let reservations = querySnapshot?.documents.compactMap { doc in
                    try? doc.data(as: Reservation.self)
                } ?? []

                continuation.yield(reservations)
            }

        continuation.onTermination = { _ in
            listener.remove()
        }
    }
}

TypeScript Pattern (WebSocket/SSE):

class RealtimeReservationService {
	private eventSource: EventSource | null = null;
	private listeners: Set<(reservations: Reservation[]) => void> = new Set();

	subscribe(callback: (reservations: Reservation[]) => void): () => void {
		this.listeners.add(callback);

		if (!this.eventSource) {
			this.startListening();
		}

		// Return cleanup function
		return () => {
			this.listeners.delete(callback);
			if (this.listeners.size === 0) {
				this.stopListening();
			}
		};
	}

	private startListening(): void {
		this.eventSource = new EventSource('/api/reservations/stream');

		this.eventSource.onmessage = event => {
			const reservations = ReservationSchema.array().parse(
				JSON.parse(event.data),
			);
			this.listeners.forEach(callback => callback(reservations));
		};
	}

	private stopListening(): void {
		this.eventSource?.close();
		this.eventSource = null;
	}
}

⚑ Performance Migration Strategy

Swift App Performance Characteristics

TypeScript Performance Goals

Performance Migration Strategies:

  1. Caching Strategy:

    // TanStack Query for server state caching
    const queryClient = new QueryClient({
    	defaultOptions: {
    		queries: {
    			staleTime: 30_000, // 30 seconds
    			cacheTime: 5 * 60_000, // 5 minutes
    			refetchOnWindowFocus: false,
    		},
    	},
    });
    
  2. Optimistic Updates:

    const addReservationMutation = useMutation({
    	mutationFn: reservationService.addReservation,
    	onMutate: async newReservation => {
    		// Optimistically update UI
    		queryClient.setQueryData(['reservations'], old => [
    			...(old || []),
    			newReservation,
    		]);
    	},
    	onError: (err, variables, context) => {
    		// Rollback on error
    		queryClient.setQueryData(
    			['reservations'],
    			context?.previousReservations,
    		);
    	},
    });
    
  3. Efficient Re-renders:

    // Memoize expensive calculations
    const sortedReservations = useMemo(() =>
      reservations.sort((a, b) => a.startTime.localeCompare(b.startTime)),
      [reservations]
    );
    
    // Prevent unnecessary re-renders
    const ReservationRow = memo(({ reservation }: { reservation: Reservation }) => {
      return <div>{reservation.name} - {reservation.startTime}</div>;
    });
    

πŸ§ͺ Testing Strategy Migration

Swift Testing Approach

TypeScript Testing Strategy

// Unit tests with Vitest
describe('ReservationService', () => {
	it('should validate time slot conflicts', async () => {
		const service = new ReservationService(mockRepo);
		const reservation = createMockReservation();

		mockRepo.findConflicts.mockResolvedValue([existingReservation]);

		await expect(service.addReservation(reservation)).rejects.toThrow(
			'Time slot conflict',
		);
	});
});

// Integration tests
describe('Reservation API', () => {
	it('should create and retrieve reservations', async () => {
		const response = await request(app)
			.post('/api/reservations')
			.send(mockReservationData)
			.expect(201);

		expect(response.body).toMatchObject(expectedReservation);
	});
});

// E2E tests with Playwright
test('should create reservation through UI', async ({ page }) => {
	await page.goto('/reservations');
	await page.click('[data-testid=add-reservation]');
	await page.fill('[name=guestName]', 'John Doe');
	await page.click('[data-testid=save-reservation]');

	await expect(page.locator('.reservation-row')).toContainText('John Doe');
});

πŸ“‹ Migration Checklist

Pre-Migration βœ…

Core Migration 🎯

UI Migration πŸ”²

Real-Time Features πŸ”²

Production Readiness πŸ”²


πŸ” Risk Assessment & Mitigation

High-Risk Areas

Real-Time Synchronization:

Performance Under Load:

Complex Business Logic:

Medium-Risk Areas

Cross-Browser Compatibility:

Mobile Experience:

Low-Risk Areas

TypeScript Migration:

Package Management:


This migration strategy provides a roadmap from the current Swift iOS app to a modern TypeScript web application while preserving the proven business value and expanding the potential user base.