Construir una Plataforma de Alquiler Vacacional con Next.js y Supabase
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:
- El anfitrión se incorpora a través de Stripe Connect Express (Stripe maneja la UI de verificación de identidad)
- El huésped paga a través de Stripe Payment Intents
- El pago se retiene hasta check-in + 24 horas
- 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 consizesysrcSetapropiados - Carga perezosa todo por debajo del pliegue
- Usar placeholder
blurcon 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.