Tu visitante se desplaza más allá de la propiedad #47 cuando el calendario de disponibilidad se congela durante tres segundos. Cierran la pestaña. Esos son $180 en comisiones que nunca verás—y sucedió porque tu tabla de reservas se bloqueó durante una conversión de zona horaria. He visto esto desarrollarse en dos plataformas de alquiler vacacional en vivo, ambas construidas con Next.js y Supabase, ambas procesando pagos reales a través de Stripe Connect. Una llegó a 3,000 listados en ocho meses. La otra casi colapsa con 400 porque el fundador omitió la seguridad a nivel de fila y permitió que los huéspedes vieran el historial de reservas de los demás. La diferencia no fue talento o presupuesto—fueron decisiones de esquema de base de datos tomadas en la semana dos. Aquí está la arquitectura que sobrevivió, los disparadores de Supabase que previnieron doble reserva a las 11 PM un sábado, y por qué este stack maneja la complejidad de alquiler a corto plazo mejor que el monolito Rails que todos aún recomiendan.

Se proyecta que el mercado de alquiler vacacional alcance $113.9 mil millones para 2027 (Statista). Airbnb posee una parte masiva, pero plataformas de nicho — estadías amigables con mascotas, villas de lujo, retiros rurales, casas de surf — están prosperando porque sirven a audiencias específicas mejor que cualquier generalista. No necesitas vencer a Airbnb. Necesitas servirles mejor en tu nicho.

Por qué Next.js + Supabase para una Plataforma de Alquiler

Seré directo: podrías construir esto con docenas de stacks diferentes. Laravel, Rails, Django — todas opciones correctas. Pero la combinación Next.js + Supabase toca un punto dulce específicamente para plataformas de alquiler.

Next.js te da:

  • Renderizado del lado del servidor para SEO (las páginas de listado necesitan clasificarse)
  • App Router con React Server Components para cargas iniciales rápidas
  • Rutas API para lógica backend sin un servidor separado
  • Optimización de imágenes integrada (crítica para fotos de propiedades)
  • Regeneración Estática Incremental para páginas de listado que rara vez cambian

Supabase te da:

  • PostgreSQL con PostGIS para consultas geográficas ("muéstrame alquileres dentro de 20km")
  • Row Level Security (RLS) que realmente funciona para aplicaciones multi-tenant
  • Suscripciones en tiempo real para mensajería y actualizaciones de reserva
  • Autenticación integrada con proveedores OAuth
  • Almacenamiento para imágenes de propiedades con entrega CDN
  • Edge Functions para lógica empresarial sin servidor

La característica realmente asesina es que Supabase es solo Postgres bajo el capó. Cuando superes la oferta administrada de Supabase (o necesites hacerlo), puedes migrar a cualquier host de Postgres. Sin encierro de proveedor en tu activo más crítico — tus datos.

Si estás evaluando frameworks, nuestro equipo de desarrollo Next.js ha desplegado varias plataformas en este mismo stack.

Descripción General de la Arquitectura del Sistema

Aquí está la arquitectura de alto nivel que ha funcionado bien en múltiples proyectos:

┌─────────────────────────────────────────────┐
│         Aplicación Next.js                  │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐  │
│  │  Páginas │  │   API    │  │  Servidor  │  │
│  │ (SSR/ISR)│  │  Rutas   │  │ Componentes│  │
│  └────┬─────┘  └────┬─────┘  └─────┬──────┘  │
│       │              │              │         │
│  ┌────▼──────────────▼──────────────▼──────┐  │
│  │      SDK de Cliente Supabase            │  │
│  └────────────────┬────────────────────────┘  │
└───────────────────┼───────────────────────────┘
                    │
         ┌──────────▼──────────┐
         │     Supabase        │
         │  ┌──────────────┐   │
         │  │  PostgreSQL   │   │
         │  │  + PostGIS    │   │
         │  ├──────────────┤   │
         │  │  Autenticación│  │
         │  ├──────────────┤   │
         │  │  Almacenamiento│ │
         │  ├──────────────┤   │
         │  │  Tiempo Real   │  │
         │  ├──────────────┤   │
         │  │  Edge Funcs   │   │
         │  └──────────────┘   │
         └─────────────────────┘
                    │
         ┌──────────▼──────────┐
         │  Servicios Externos  │
         │  • Stripe Connect    │
         │  • Mapbox/Google Maps│
         │  • SendGrid/Resend   │
         │  • Cloudflare CDN    │
         └─────────────────────┘

La decisión arquitectónica clave es usar Supabase Edge Functions para operaciones críticas de negocio como creación de reserva y webhooks de pago, mientras se mantienen las rutas API de Next.js para tareas más ligeras como consultas de búsqueda y validación de formularios. Esta separación importa cuando un webhook de Stripe se dispara y necesitas garantizar que el estado de la reserva se actualice atómicamente.

Diseño del Esquema de Base de Datos

Aquí es donde la mayoría de las plataformas de alquiler se equivocan temprano y pagan por ello más tarde. Aquí hay un esquema que ha sobrevivido al tráfico de producción:

-- Habilitar PostGIS
create extension if not exists postgis;

-- Los perfiles extienden auth.users de Supabase
create table public.profiles (
  id uuid references auth.users on delete cascade primary key,
  full_name text not null,
  avatar_url text,
  phone text,
  role text check (role in ('guest', 'host', 'admin')) default 'guest',
  stripe_account_id text, -- Para pagos al anfitrión
  identity_verified boolean default false,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Propiedades/Listados
create table public.properties (
  id uuid default gen_random_uuid() primary key,
  host_id uuid references public.profiles(id) not null,
  title text not null,
  slug text unique not null,
  description text,
  property_type text check (property_type in (
    'apartment', 'house', 'villa', 'cabin', 'unique'
  )),
  max_guests int not null default 1,
  bedrooms int not null default 0,
  beds int not null default 1,
  bathrooms numeric(3,1) not null default 1,
  amenities text[] default '{}',
  -- Precios
  base_price_cents int not null,
  cleaning_fee_cents int default 0,
  currency text default 'USD',
  -- Ubicación
  address_line1 text,
  city text not null,
  state text,
  country text not null,
  postal_code text,
  location geography(Point, 4326), -- PostGIS
  -- Estado
  status text check (status in ('draft', 'listed', 'unlisted', 'suspended'))
    default 'draft',
  -- Políticas
  cancellation_policy text check (cancellation_policy in (
    'flexible', 'moderate', 'strict'
  )) default 'moderate',
  check_in_time time default '15:00',
  check_out_time time default '11:00',
  min_nights int default 1,
  max_nights int default 365,
  -- Metadatos
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Índice espacial para consultas geo
create index properties_location_idx
  on public.properties using gist (location);

-- Disponibilidad y anulaciones de precios
create table public.availability (
  id uuid default gen_random_uuid() primary key,
  property_id uuid references public.properties(id) on delete cascade,
  date date not null,
  available boolean default true,
  price_override_cents int, -- null = usar precio base
  min_nights_override int,
  unique(property_id, date)
);

-- Reservas
create table public.bookings (
  id uuid default gen_random_uuid() primary key,
  property_id uuid references public.properties(id) not null,
  guest_id uuid references public.profiles(id) not null,
  check_in date not null,
  check_out date not null,
  guests_count int not null default 1,
  -- Snapshot de precios (inmutable después de la creación)
  nightly_rate_cents int not null,
  nights int not null,
  subtotal_cents int not null,
  cleaning_fee_cents int not null,
  service_fee_cents int not null,
  total_cents int not null,
  currency text default 'USD',
  -- Estado
  status text check (status in (
    'pending', 'confirmed', 'cancelled', 'completed', 'disputed'
  )) default 'pending',
  -- Pago
  stripe_payment_intent_id text,
  stripe_transfer_id text,
  paid_at timestamptz,
  -- Timestamps
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  -- Prevenir doble reserva a nivel DB
  exclude using gist (
    property_id with =,
    daterange(check_in, check_out) with &&
  ) where (status in ('pending', 'confirmed'))
);

Esa restricción exclude en la tabla de reservas? Esa es la línea más importante en todo el esquema. Previene doble reserva a nivel de base de datos usando una restricción de exclusión GiST. Sin condiciones de carrera. Sin correos "lo siento, alguien lo reservó 2 segundos antes que tú". La base de datos literalmente no permitirá rangos de fecha solapados para la misma propiedad.

Necesitarás la extensión btree_gist:

create extension if not exists btree_gist;

Construyendo el Motor de Reserva

El flujo de reserva es el corazón de cualquier plataforma de alquiler. Aquí está cómo lo estructuro:

Paso 1: Verificación de Disponibilidad

// lib/bookings/check-availability.ts
import { createClient } from '@/lib/supabase/server';

export async function checkAvailability(
  propertyId: string,
  checkIn: string,
  checkOut: string
) {
  const supabase = await createClient();

  // Verificar fechas bloqueadas
  const { data: blockedDates } = await supabase
    .from('availability')
    .select('date')
    .eq('property_id', propertyId)
    .eq('available', false)
    .gte('date', checkIn)
    .lt('date', checkOut);

  if (blockedDates && blockedDates.length > 0) {
    return { available: false, reason: 'Algunas fechas no están disponibles' };
  }

  // Verificar reservas existentes (cinturón + tirantes con la restricción DB)
  const { data: conflicts } = await supabase
    .from('bookings')
    .select('id')
    .eq('property_id', propertyId)
    .in('status', ['pending', 'confirmed'])
    .or(`check_in.lt.${checkOut},check_out.gt.${checkIn}`);

  if (conflicts && conflicts.length > 0) {
    return { available: false, reason: 'Fechas ya reservadas' };
  }

  return { available: true };
}

Paso 2: Cálculo de Precio

Nunca confíes en cálculos de precio del lado del cliente. Siempre recalcula en el servidor:

// lib/bookings/calculate-price.ts
export async function calculateBookingPrice(
  propertyId: string,
  checkIn: string,
  checkOut: string
) {
  const supabase = await createClient();

  const { data: property } = await supabase
    .from('properties')
    .select('base_price_cents, cleaning_fee_cents')
    .eq('id', propertyId)
    .single();

  if (!property) throw new Error('Propiedad no encontrada');

  // Obtener cualquier anulación de precio para estas fechas
  const { data: overrides } = await supabase
    .from('availability')
    .select('date, price_override_cents')
    .eq('property_id', propertyId)
    .gte('date', checkIn)
    .lt('date', checkOut)
    .not('price_override_cents', 'is', null);

  const overrideMap = new Map(
    overrides?.map(o => [o.date, o.price_override_cents]) ?? []
  );

  // Calcular precios noche por noche
  let subtotal = 0;
  const start = new Date(checkIn);
  const end = new Date(checkOut);
  const nights = Math.round(
    (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)
  );

  for (let i = 0; i < nights; i++) {
    const current = new Date(start);
    current.setDate(current.getDate() + i);
    const dateStr = current.toISOString().split('T')[0];
    const rate = overrideMap.get(dateStr) ?? property.base_price_cents;
    subtotal += rate;
  }

  const serviceFee = Math.round(subtotal * 0.12); // 12% tarifa de servicio
  const total = subtotal + property.cleaning_fee_cents + serviceFee;

  return {
    nights,
    nightlyRate: property.base_price_cents,
    subtotal,
    cleaningFee: property.cleaning_fee_cents,
    serviceFee,
    total,
  };
}

Paso 3: Crear Reserva con Intención de Pago

Aquí es donde Stripe entra en juego. Uso una Server Action en Next.js 14+:

// app/actions/create-booking.ts
'use server';

import Stripe from 'stripe';
import { createClient } from '@/lib/supabase/server';
import { calculateBookingPrice } from '@/lib/bookings/calculate-price';
import { checkAvailability } from '@/lib/bookings/check-availability';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function createBooking(formData: FormData) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) throw new Error('No autenticado');

  const propertyId = formData.get('propertyId') as string;
  const checkIn = formData.get('checkIn') as string;
  const checkOut = formData.get('checkOut') as string;
  const guestsCount = parseInt(formData.get('guests') as string);

  // Re-verificar disponibilidad
  const availability = await checkAvailability(propertyId, checkIn, checkOut);
  if (!availability.available) {
    return { error: availability.reason };
  }

  // Recalcular precio del lado del servidor
  const pricing = await calculateBookingPrice(propertyId, checkIn, checkOut);

  // Crear Intención de Pago de Stripe
  const paymentIntent = await stripe.paymentIntents.create({
    amount: pricing.total,
    currency: 'usd',
    metadata: {
      propertyId,
      checkIn,
      checkOut,
      guestId: user.id,
    },
  });

  // Insertar reserva
  const { data: booking, error } = await supabase
    .from('bookings')
    .insert({
      property_id: propertyId,
      guest_id: user.id,
      check_in: checkIn,
      check_out: checkOut,
      guests_count: guestsCount,
      nightly_rate_cents: pricing.nightlyRate,
      nights: pricing.nights,
      subtotal_cents: pricing.subtotal,
      cleaning_fee_cents: pricing.cleaningFee,
      service_fee_cents: pricing.serviceFee,
      total_cents: pricing.total,
      stripe_payment_intent_id: paymentIntent.id,
      status: 'pending',
    })
    .select()
    .single();

  if (error) {
    // La restricción de exclusión detectará doble reserva
    if (error.code === '23P01') {
      return { error: 'Alguien más acaba de reservar estas fechas.' };
    }
    throw error;
  }

  return {
    bookingId: booking.id,
    clientSecret: paymentIntent.client_secret,
  };
}

Nota cómo manejamos el código de error 23P01 — ese es el error de violación de exclusión de PostgreSQL. Incluso si dos usuarios hacen clic en "Reservar" al mismo milisegundo exacto, solo una reserva se procesa.

Búsqueda y Filtrado con PostGIS

La búsqueda geográfica es imprescindible para plataformas de alquiler. Aquí hay una función de Postgres que maneja búsqueda basada en radio con filtros:

create or replace function search_properties(
  lat double precision,
  lng double precision,
  radius_km int default 50,
  min_price int default 0,
  max_price int default 100000,
  min_bedrooms int default 0,
  guest_count int default 1,
  p_check_in date default null,
  p_check_out date default null
)
returns setof public.properties
language sql stable
as $$
  select p.*
  from public.properties p
  where p.status = 'listed'
    and ST_DWithin(
      p.location,
      ST_MakePoint(lng, lat)::geography,
      radius_km * 1000
    )
    and p.base_price_cents between min_price and max_price
    and p.bedrooms >= min_bedrooms
    and p.max_guests >= guest_count
    and (
      p_check_in is null
      or not exists (
        select 1 from public.bookings b
        where b.property_id = p.id
          and b.status in ('pending', 'confirmed')
          and b.check_in < p_check_out
          and b.check_out > p_check_in
      )
    )
  order by p.location <-> ST_MakePoint(lng, lat)::geography
  limit 50;
$$;

Llamarlo desde Next.js:

const { data } = await supabase.rpc('search_properties', {
  lat: 34.0522,
  lng: -118.2437,
  radius_km: 30,
  guest_count: 4,
  p_check_in: '2026-08-01',
  p_check_out: '2026-08-07',
});

Esto se ejecuta en menos de 50ms para 100K+ listados con indexación adecuada. No se necesita Elasticsearch hasta que alcances escala mucho más grande.

Autenticación y Acceso Multi-Rol

Suabase Auth maneja el trabajo pesado. La parte complicada es la naturaleza dual de las plataformas de alquiler — alguien puede ser tanto huésped como anfitrión.

Manejo esto con un campo de rol en el perfil que se actualiza de guest a host cuando crean su primer listado, más políticas de Row Level Security:

-- Los anfitriones solo pueden editar sus propias propiedades
create policy "hosts_manage_own_properties" on public.properties
  for all using (host_id = auth.uid());

-- Los huéspedes pueden ver propiedades listadas
create policy "anyone_view_listed" on public.properties
  for select using (status = 'listed');

-- Los huéspedes solo pueden ver sus propias reservas
create policy "guests_own_bookings" on public.bookings
  for select using (guest_id = auth.uid());

-- Los anfitriones pueden ver reservas para sus propiedades
create policy "hosts_property_bookings" on public.bookings
  for select using (
    property_id in (
      select id from public.properties where host_id = auth.uid()
    )
  );

RLS es genuinamente una de las características más fuertes de Supabase para aplicaciones multi-tenant. Las reglas de seguridad viven junto a los datos, no dispersas en middleware de API.

Procesamiento de Pagos y Pagos

Usa Stripe Connect. Punto. Maneja pagos de mercado, divisiones, 1099s, KYC, y pagos internacionales. La alternativa es construir tu propio sistema de transmisión de dinero, que es... no lo hagas.

Aquí está el flujo:

  1. El anfitrión se incorpora a través de Stripe Connect Express (Stripe maneja la UI de verificación de identidad)
  2. El huésped paga a través de Stripe Payment Intents
  3. El pago se retiene hasta check-in + 24 horas
  4. Transferencia de pago al anfitrión menos tu tarifa de plataforma

Precios de Stripe Connect en 2026: 0.25% + $0.25 por pago además de tarifas de procesamiento estándar (2.9% + $0.30 por transacción). Para una reserva de $200/noche, estás mirando aproximadamente $6.50 en tarifas de Stripe. Presupuesta para ello.

Mensajería en Tiempo Real y Notificaciones

Supabase Realtime hace que la mensajería entre anfitrión y huésped sea directa:

// Suscribirse a nuevos mensajes en una conversación
const channel = supabase
  .channel(`conversation:${conversationId}`)
  .on(
    'postgres_changes',
    {
      event: 'INSERT',
      schema: 'public',
      table: 'messages',
      filter: `conversation_id=eq.${conversationId}`,
    },
    (payload) => {
      setMessages(prev => [...prev, payload.new]);
    }
  )
  .subscribe();

Para notificaciones por correo (confirmaciones de reserva, recordatorios de check-in), uso Resend o SendGrid activados desde Supabase Edge Functions a través de webhooks de base de datos. El precio de Resend comienza en $20/mes para 50K correos — más que suficiente para una plataforma en crecimiento.

Manejo de Imágenes y Rendimiento

Las fotos de propiedades hacen o rompen las tasas de conversión. Cada listado podría tener 15-30 imágenes, y necesitan cargar rápido.

Mi enfoque:

  • Cargar originales en Supabase Storage
  • Usar la API de transformación de imágenes de Supabase para redimensionamiento en tiempo real
  • Servir a través del componente <Image> de Next.js con sizes y srcSet apropiados
  • Carga perezosa todo por debajo del pliegue
  • Usar placeholder blur con vista previa base64 diminuta generada en tiempo de carga
<Image
  src={`${SUPABASE_URL}/storage/v1/render/image/public/properties/${imageId}?width=800&quality=80`}
  alt={property.title}
  width={800}
  height={600}
  placeholder="blur"
  blurDataURL={image.blur_hash}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

Este enfoque entrega LCP por debajo de un segundo en páginas de listado con 20+ fotos.

Despliegue y Consideraciones de Escalabilidad

Para despliegue, Vercel es la opción natural para Next.js. Pero aquí hay un matiz que la mayoría de artículos omiten: usa Vercel's Edge Runtime con moderación para una plataforma de alquiler. El flujo de reserva necesita tiempo de ejecución de Node.js para SDK de Stripe y operaciones complejas de base de datos. Edge es excelente para middleware (redirecciones geográficas, verificaciones de autenticación) pero no para lógica de negocio.

Opción de Despliegue Mejor Para Costo Mensual (estimado)
Vercel Pro + Supabase Pro MVP a 10K MAU $45 - $100
Vercel Pro + Equipo Supabase 10K-100K MAU $200 - $500
Next.js Autoalojado + Supabase Pro Optimización de costo $100 - $300
AWS/GCP + Supabase Autoalojado Control total a escala $500+

Supabase Pro comienza en $25/mes por proyecto e incluye 8GB de base de datos, 250GB de ancho de banda y 100GB de almacenamiento. Suficiente para la mayoría de MVPs y plataformas en etapa inicial.

Desglose de Costos: Lo que Realmente Gastarás

Aquí está lo que cuesta realmente construir y ejecutar una plataforma de alquiler vacacional en 2026:

Elemento Costo MVP Costo de Ejecución Mensual
Supabase Pro - $25
Vercel Pro - $20
Stripe Connect - ~2.9% + $0.30/transacción
Mapbox/Google Maps - $0-200 (basado en uso)
Resend (correo) - $20
Dominio + DNS (Cloudflare) $15/año $0
Desarrollo (agencia) $40K-120K -
Desarrollo (desarrollador único) $15K-40K -
Total Infraestructura Mensual - ~$65-265

Compáralo con construir en una plataforma SaaS como Sharetribe ($299-599/mes) o Guesty, y la economía del desarrollo personalizado comienza a tener sentido una vez que tengas tráfico real.

Si hablas en serio sobre construir una plataforma de alquiler y quieres desarrolladores experimentados que hayan desplegado este tipo exacto de producto, comunícate con nuestro equipo o consulta nuestro página de precios para estimaciones de proyecto. Nos especializamos en desarrollo de CMS headless y aplicaciones complejas de Next.js.

Preguntas Frecuentes

¿Cuánto tiempo toma construir una plataforma de alquiler vacacional como Airbnb? Un MVP funcional con listados, búsqueda, reservas, pagos y mensajería toma 3-5 meses con un equipo competente de 2-3 desarrolladores. Un desarrollador único podría necesitar 6-9 meses. Esto te lleva al lanzamiento — no a paridad de características con Airbnb, que tiene 15+ años de desarrollo detrás. Enfócate primero en tus características de nicho.

¿Es Supabase lo suficientemente escalable para una plataforma de alquiler de producción? Sí, hasta cierto punto. Supabase Pro maneja decenas de miles de usuarios concurrentes cómodamente. Su plan Team ($599/mes) soporta significativamente más. Instagram se ejecutó en un servidor PostgreSQL único durante años. Tu cuello de botella será ajuste producto-mercado mucho antes de ser escala de base de datos. Cuando realmente superes Supabase, tus datos están en PostgreSQL estándar — la migración es directa.

¿Cómo previenen la doble reserva en un sistema de alquiler vacacional? Usa restricciones de exclusión de PostgreSQL con la extensión btree_gist. Esto se aplica a nivel de base de datos que no dos reservas activas pueden tener rangos de fecha solapados para la misma propiedad. Es el único método confiable — las verificaciones a nivel de aplicación tienen condiciones de carrera. El ejemplo de esquema anterior muestra exactamente cómo implementar esto.

¿Debería usar Stripe Connect o construir mi propio sistema de pagos? Stripe Connect. Siempre. Construir tu propio sistema de división de pagos para un mercado implica licencias de transmisión de dinero, cumplimiento de KYC/AML, informes fiscales internacionales y prevención de fraude. Stripe maneja todo esto. Las tarifas (aproximadamente 3.2% por transacción) valen la pena. Siempre puedes negociar tarifas una vez que estés procesando volumen significativo.

¿Cuál es la mejor manera de manejar búsqueda de propiedades con mapas? PostGIS con Supabase para consultas backend, y Mapbox GL JS o Google Maps JavaScript API para el frontend. Las consultas espaciales de PostGIS con índices GiST apropiados manejan búsquedas de radio y caja delimitadora en milisegundos. El precio de Mapbox comienza con un nivel gratuito generoso (50K cargas de mapa/mes). Google Maps cobra $7 por 1000 cargas de mapa dinámicas después del crédito mensual de $200.

¿Cómo manejo precios estacionales y tarifas dinámicas? Usa una tabla de anulación de disponibilidad/precios basada en fechas junto con el precio base de la propiedad. Para cada noche de una reserva, verifica si hay una anulación de precio para esa fecha específica. Si no, retrocede al precio base. Esto maneja tarifas estacionales, premios de fin de semana, precios navideños y descuentos de último minuto. Algunas plataformas también se integran con PriceLabs ($19.99/listado/mes) o Beyond Pricing para precios dinámicos automatizados.

¿Es Next.js mejor que Astro para una plataforma de alquiler? Para una plataforma de alquiler completa con flujos de reserva interactivos, mensajería y paneles — Next.js gana. La aplicación necesita interactividad significativa del lado del cliente. Astro excela en sitios con mucho contenido e interactividad mínima (consulta nuestras capacidades de desarrollo Astro). Dicho esto, si estás construyendo un sitio solo de listados sin reservas (como un directorio), el rendimiento de Astro sería excepcional.

¿Qué pasa con aplicaciones móviles — ¿necesito React Native también? No para tu MVP. Construye primero la aplicación Next.js como PWA receptiva (Progressive Web App). Agrega notificaciones push, almacenamiento caché sin conexión y un indicador "Agregar a Pantalla de Inicio". Esto cubre el 80% de casos de uso móvil. Una vez que hayas validado el producto y tengas ingresos reales, invierte en aplicaciones nativas. Muchas plataformas de alquiler de nicho exitosas (Hipcamp, Glamping Hub) se lanzaron primero con web y agregaron aplicaciones nativas después.