Construye una Plataforma de Alquiler Vacacional con Next.js y Supabase
He pasado los últimos 18 meses ayudando a dos clientes diferentes a construir plataformas de alquiler vacacional. No son proyectos de juguete — son negocios reales con miles de listados, procesamiento de pagos y la clase de lógica de reserva que te hará cuestionarte tus decisiones profesionales a las 2 AM. Aquí está lo que he aprendido sobre cómo construir una plataforma estilo Airbnb con Next.js y Supabase, y por qué este stack es genuinamente viable para startups que ingresan al espacio de alquiler a corto plazo en 2025.
Se proyecta que el mercado de alquiler vacacional alcance $113.9 mil millones para 2027 (Statista). Airbnb posee un enorme segmento, pero las plataformas de nicho — estadías para mascotas, villas de lujo, retiros rurales, casas de surf — están prosperando porque sirven mejor a audiencias específicas que un generalista jamás podría. No necesitas vencer a Airbnb. Necesitas servirles mejor en tu nicho.
Tabla de Contenidos
- Por qué Next.js + Supabase para una Plataforma de Alquiler
- Descripción General de la Arquitectura del Sistema
- Diseño del Esquema de Base de Datos
- Construir el Motor de Reservas
- Búsqueda y Filtrado con PostGIS
- Autenticación y Acceso Multirol
- Procesamiento de Pagos y Pagos
- Mensajería y Notificaciones en Tiempo Real
- Gestión de Imágenes y Rendimiento
- Consideraciones de Implementación y Escalabilidad
- Desglose de Costos: Lo Que Realmente Gastarás
- FAQ

Por qué Next.js + Supabase para una Plataforma de Alquiler
Déjame ser directo: podrías construir esto con docenas de stacks diferentes. Laravel, Rails, Django — todas son buenas opciones. Pero la combinación Next.js + Supabase golpea un punto dulce para plataformas de alquiler específicamente.
Next.js te da:
- Renderización del lado del servidor para SEO (las páginas de listados necesitan clasificarse)
- App Router con React Server Components para cargas iniciales rápidas
- Rutas API para lógica de backend sin un servidor separado
- Optimización de imágenes incorporada (crítica para fotos de propiedades)
- Regeneración Estática Incremental para páginas de listados 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 reservas
- 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 gestionada de Supabase (o necesites hacerlo), puedes migrar a cualquier host de Postgres. Sin vendor lock-in en tu activo más crítico — tus datos.
Si estás evaluando frameworks, nuestro equipo de desarrollo Next.js ha lanzado varias plataformas en este stack exacto.
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 │ │ Server │ │
│ │ (SSR/ISR)│ │ Routes │ │ Components │ │
│ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
│ │ │ │ │
│ ┌────▼──────────────▼──────────────▼──────┐ │
│ │ SDK del Cliente Supabase │ │
│ └────────────────┬────────────────────────┘ │
└───────────────────┼───────────────────────────┘
│
┌──────────▼──────────┐
│ Supabase │
│ ┌──────────────┐ │
│ │ PostgreSQL │ │
│ │ + PostGIS │ │
│ ├──────────────┤ │
│ │ Auth │ │
│ ├──────────────┤ │
│ │ Storage │ │
│ ├──────────────┤ │
│ │ Realtime │ │
│ ├──────────────┤ │
│ │ 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 para el negocio como creación de reservas y webhooks de pago, mientras mantienes 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 de forma atómica.
Diseño del Esquema de Base de Datos
Aquí es donde la mayoría de plataformas de alquiler la tienen mal temprano y lo pagan después. Aquí hay un esquema que ha sobrevivido al tráfico de producción:
-- Habilitar PostGIS
create extension if not exists postgis;
-- 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 del 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,
-- Metadata
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Índice espacial para consultas geográficas
create index properties_location_idx
on public.properties using gist (location);
-- Disponibilidad e invalidaciones 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 de BD
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 reservas 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 fechas superpuestos para la misma propiedad.
Necesitarás la extensión btree_gist:
create extension if not exists btree_gist;

Construir el Motor de Reservas
El flujo de reserva es el corazón de cualquier plataforma de alquiler. Así es como 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 de BD)
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 invalidació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); // Comisión de servicio del 12%
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 entra Stripe. 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 };
}
// Re-calcular 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 capturará doble reservas
if (error.code === '23P01') {
return { error: 'Estas fechas fueron reservadas hace poco por otra persona.' };
}
throw error;
}
return {
bookingId: booking.id,
clientSecret: paymentIntent.client_secret,
};
}
Nota cómo manejamos el código de error 23P01 — ese es la 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 geo es innegociable 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;
$$;
Llámalo 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: '2025-08-01',
p_check_out: '2025-08-07',
});
Esto se ejecuta en menos de 50ms para 100K+ listados con indexación adecuada. Sin Elasticsearch necesario hasta que alcances escala mucho más grande.
Autenticación y Acceso Multirol
Supabase Auth maneja el trabajo pesado. La parte complicada es la naturaleza dual de plataformas de alquiler — alguien puede ser tanto huésped como anfitrión.
Lo manejo 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 dispersadas por middleware API.
Procesamiento de Pagos y Pagos
Usa Stripe Connect. Punto final. Maneja pagos de marketplace, división, 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 registra a través de Stripe Connect Express (Stripe maneja la IU 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
- El pago se transfiere al anfitrión menos tu comisión de plataforma
Precios de Stripe Connect en 2025: 0.25% + $0.25 por pago además de las tarifas de procesamiento estándar (2.9% + $0.30 por carga). Para una reserva de $200/noche, estás viendo aproximadamente $6.50 en tarifas de Stripe. Presupuesta para ello.
Mensajería y Notificaciones en Tiempo Real
Supabase Realtime hace sencilla la mensajería anfitrión-huésped:
// 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 activadas desde Supabase Edge Functions a través de webhooks de base de datos. El precio de Resend comienza en $20/mes para 50K correos electrónicos — más que suficiente para una plataforma en crecimiento.
Gestión 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:
- Subir originales a Supabase Storage
- Usar API de transformación de imágenes de Supabase para redimensionamiento sobre la marcha
- Servir a través del componente
<Image>de Next.js consizesysrcSetapropiados - Lazy-load todo lo que está debajo del pliegue
- Usar placeholder
blurcon una vista previa base64 pequeña 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 de menos de un segundo en páginas de listados con 20+ fotos.
Consideraciones de Implementación y Escalabilidad
Para implementación, Vercel es la opción natural para Next.js. Pero aquí hay un matiz que la mayoría de artículos se saltan: usa Vercel's Edge Runtime con moderación para una plataforma de alquiler. El flujo de reserva necesita Node.js runtime para Stripe SDK y operaciones complejas de base de datos. Edge es genial para middleware (geo-redirecciones, comprobaciones de auth) pero no para lógica empresarial.
| Opción de Implementación | Mejor Para | Costo Mensual (estimado) |
|---|---|---|
| Vercel Pro + Supabase Pro | MVP a 10K MAU | $45 - $100 |
| Vercel Pro + Supabase Team | 10K-100K MAU | $200 - $500 |
| Next.js Auto-hospedado + Supabase Pro | Optimización de costos | $100 - $300 |
| AWS/GCP + Supabase Auto-hospedado | Control total a escala | $500+ |
Supabase Pro comienza en $25/mes por proyecto e incluye 8GB base de datos, 250GB ancho de banda, y 100GB almacenamiento. Suficiente para la mayoría de MVPs y plataformas tempranas.
Desglose de Costos: Lo Que Realmente Gastarás
Aquí está lo que una plataforma real de alquiler vacacional cuesta construir y ejecutar en 2025:
| Elemento | Costo MVP | Costo Mensual de Funcionamiento |
|---|---|---|
| Supabase Pro | - | $25 |
| Vercel Pro | - | $20 |
| Stripe Connect | - | ~2.9% + $0.30/transacción |
| Mapbox/Google Maps | - | $0-200 (basado en uso) |
| Resend (email) | - | $20 |
| Dominio + DNS (Cloudflare) | $15/año | $0 |
| Desarrollo (agencia) | $40K-120K | - |
| Desarrollo (solo dev) | $15K-40K | - |
| Total Infra 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 cualquier tracción real.
Si hablas en serio sobre construir una plataforma de alquiler y quieres desarrolladores experimentados que hayan lanzado este tipo exacto de producto, ponte en contacto con nuestro equipo o consulta nuestra página de precios para estimaciones de proyectos. Nos especializamos en desarrollo de CMS headless y aplicaciones Next.js complejas.
FAQ
¿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 calificado de 2-3 desarrolladores. Un desarrollador solo podría necesitar 6-9 meses. Esto te pone a lanzar — no paridad de características con Airbnb, que tiene 15+ años de desarrollo detrás. Enfócate en las características de tu nicho primero.
¿Es Supabase suficientemente escalable para una plataforma de alquiler en 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 único servidor PostgreSQL por años. Tu cuello de botella será product-market fit mucho antes que sea escala de base de datos. Cuando superes Supabase, tus datos están en PostgreSQL estándar — la migración es directa.
¿Cómo previene doble reserva en un sistema de alquiler vacacional?
Usa restricciones de exclusión de PostgreSQL con la extensión btree_gist. Esto impone a nivel de base de datos que no hay dos reservas activas pueden tener rangos de fechas superpuestos para la misma propiedad. Es el único método confiable — las comprobaciones a nivel de aplicación tienen condiciones de carrera. El ejemplo de esquema arriba 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 marketplace implica licencias de transmisión de dinero, cumplimiento de KYC/AML, reporte fiscal internacional, y prevención de fraude. Stripe maneja todo esto. Las tarifas (aproximadamente 3.2% por transacción) valen la pena. Siempre puedes negociar tasas 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 las consultas de backend, y Mapbox GL JS o Google Maps JavaScript API para el frontend. Las consultas espaciales de PostGIS con índices GiST adecuados manejan búsquedas de radio y bounding-box en milisegundos. El precio de Mapbox comienza con un nivel libre 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 manejaré precios estacionales y tasas dinámicas?
Usa una tabla de disponibilidad/invalidación de precios basada en fechas junto con el precio base de propiedad. Para cada noche de una reserva, comprueba si hay una invalidación de precio para esa fecha específica. Si no, retrocede al precio base. Esto maneja tarifas estacionales, premios de fin de semana, precios de vacaciones, y descuentos de último minuto. Algunas plataformas también 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 dashboards — Next.js gana. La aplicación necesita interactividad significativa del lado del cliente. Astro destaca en sitios con mucho contenido con 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é hay de aplicaciones móviles — ¿necesito React Native también?
No para tu MVP. Construye la aplicación Next.js como un PWA receptivo (Progressive Web App) primero. Añade notificaciones push, almacenamiento en caché offline, y una solicitud "Añadir a Pantalla de Inicio". Esto cubre el 80% de los casos de uso móviles. 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) lanzaron web-primero y añadieron aplicaciones nativas después.