Passei os últimos 18 meses ajudando dois clientes diferentes a construir plataformas de aluguel de temporada. Não são projetos de brinquedo — são negócios reais com milhares de anúncios, processamento de pagamentos, e a lógica de reserva que te faz questionar suas escolhas de carreira às 2 da manhã. Aqui está o que aprendi sobre construir uma plataforma estilo Airbnb com Next.js e Supabase, e por que esse stack é genuinamente viável para startups que entram no espaço de aluguel de curta duração em 2025.

O mercado de aluguel de temporada deve atingir $113,9 bilhões até 2027 (Statista). Airbnb possui uma fatia massiva, mas plataformas de nicho — hospedagens pet-friendly, vilas de luxo, retiros rurais, casas para surfe — estão prosperando porque servem públicos específicos melhor do que um generalista jamais poderia. Você não precisa vencer o Airbnb. Você precisa melhor servi-los em seu nicho.

Índice

Build a Vacation Rental Platform with Next.js and Supabase

Por que Next.js + Supabase para uma Plataforma de Aluguel

Deixa eu ser direto: você poderia construir isso com dezenas de stacks diferentes. Laravel, Rails, Django — todas são boas escolhas. Mas a combinação Next.js + Supabase atinge um doce ponto para plataformas de aluguel especificamente.

Next.js te dá:

  • Renderização no lado do servidor para SEO (páginas de anúncios precisam rankear)
  • App Router com React Server Components para carregamentos iniciais rápidos
  • API routes para lógica de backend sem servidor separado
  • Otimização de imagem integrada (crítica para fotos de propriedades)
  • Regeneração Estática Incremental para páginas de anúncios que raramente mudam

Supabase te dá:

  • PostgreSQL com PostGIS para queries geográficas ("mostre-me aluguéis dentro de 20km")
  • Row Level Security (RLS) que realmente funciona para apps multi-tenant
  • Assinaturas em tempo real para mensagens e atualizações de reservas
  • Auth integrada com provedores OAuth
  • Storage para imagens de propriedades com entrega via CDN
  • Edge Functions para lógica de negócio serverless

A verdadeira feature matadora é que Supabase é apenas Postgres sob o capô. Quando você crescer além da oferta gerenciada do Supabase (ou precisar de mais), você pode migrar para qualquer host Postgres. Sem vendor lock-in no seu ativo mais crítico — seus dados.

Se você está avaliando frameworks, nossa equipe de desenvolvimento Next.js já entregou várias plataformas nesse mesmo stack.

Visão Geral da Arquitetura do Sistema

Aqui está a arquitetura de alto nível que funcionou bem em múltiplos projetos:

┌─────────────────────────────────────────────┐
│              Next.js Application             │
│  ┌─────────┐  ┌──────────┐  ┌────────────┐  │
│  │  Pages   │  │   API    │  │  Server    │  │
│  │ (SSR/ISR)│  │  Routes  │  │ Components │  │
│  └────┬─────┘  └────┬─────┘  └─────┬──────┘  │
│       │              │              │         │
│  ┌────▼──────────────▼──────────────▼──────┐  │
│  │         Supabase Client SDK             │  │
│  └────────────────┬────────────────────────┘  │
└───────────────────┼───────────────────────────┘
                    │
         ┌──────────▼──────────┐
         │     Supabase        │
         │  ┌──────────────┐   │
         │  │  PostgreSQL   │   │
         │  │  + PostGIS    │   │
         │  ├──────────────┤   │
         │  │  Auth         │   │
         │  ├──────────────┤   │
         │  │  Storage      │   │
         │  ├──────────────┤   │
         │  │  Realtime     │   │
         │  ├──────────────┤   │
         │  │  Edge Funcs   │   │
         │  └──────────────┘   │
         └─────────────────────┘
                    │
         ┌──────────▼──────────┐
         │  External Services   │
         │  • Stripe Connect    │
         │  • Mapbox/Google Maps│
         │  • SendGrid/Resend   │
         │  • Cloudflare CDN    │
         └─────────────────────┘

A decisão arquitetônica chave é usar Supabase Edge Functions para operações críticas de negócio como criação de reservas e webhooks de pagamento, enquanto mantém as API routes do Next.js para tarefas mais leves como queries de busca e validação de formulário. Essa separação importa quando um webhook do Stripe chega e você precisa garantir que o estado da reserva atualize atomicamente.

Design do Schema do Banco de Dados

É aqui onde a maioria das plataformas de aluguel erra no início e paga por isso depois. Aqui está um schema que sobreviveu ao tráfego de produção:

-- Ativar PostGIS
create extension if not exists postgis;

-- Profiles estendem auth.users do 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 pagamentos de hosts
  identity_verified boolean default false,
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

-- Propriedades/Anúncios
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 '{}',
  -- Preço
  base_price_cents int not null,
  cleaning_fee_cents int default 0,
  currency text default 'USD',
  -- Localização
  address_line1 text,
  city text not null,
  state text,
  country text not null,
  postal_code text,
  location geography(Point, 4326), -- PostGIS
  -- Status
  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,
  -- Metadados
  created_at timestamptz default now(),
  updated_at timestamptz default now()
);

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

-- Disponibilidade e overrides de preço
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 preço 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 preço (imutável após criação)
  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',
  -- Status
  status text check (status in (
    'pending', 'confirmed', 'cancelled', 'completed', 'disputed'
  )) default 'pending',
  -- Pagamento
  stripe_payment_intent_id text,
  stripe_transfer_id text,
  paid_at timestamptz,
  -- Timestamps
  created_at timestamptz default now(),
  updated_at timestamptz default now(),
  -- Prevenir double bookings no nível do DB
  exclude using gist (
    property_id with =,
    daterange(check_in, check_out) with &&
  ) where (status in ('pending', 'confirmed'))
);

Aquela restrição exclude na tabela de reservas? Essa é a linha mais importante em todo o schema. Ela previne double bookings no nível do banco de dados usando uma restrição de exclusão GiST. Sem race conditions. Sem emails "desculpa, alguém reservou 2 segundos antes de você". O banco de dados literalmente não permitirá intervalos de datas sobrepostos para a mesma propriedade.

Você precisará da extensão btree_gist:

create extension if not exists btree_gist;

Build a Vacation Rental Platform with Next.js and Supabase - architecture

Construindo o Motor de Reservas

O fluxo de reserva é o coração de qualquer plataforma de aluguel. Aqui está como estruturo:

Passo 1: Verificação de Disponibilidade

// 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 datas 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: 'Algumas datas não estão disponíveis' };
  }

  // Verificar reservas existentes (cinto e suspensório com a restrição do 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: 'Datas já reservadas' };
  }

  return { available: true };
}

Passo 2: Cálculo de Preço

Nunca confie em cálculos de preço no lado do cliente. Sempre recalcule no 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('Propriedade não encontrada');

  // Obter overrides de preço para essas datas
  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 preço noite-por-noite
  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% taxa de serviço
  const total = subtotal + property.cleaning_fee_cents + serviceFee;

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

Passo 3: Criar Reserva com Payment Intent

É aqui que Stripe entra em cena. Uso uma Server Action no 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('Não 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 disponibilidade
  const availability = await checkAvailability(propertyId, checkIn, checkOut);
  if (!availability.available) {
    return { error: availability.reason };
  }

  // Re-calcular preço no servidor
  const pricing = await calculateBookingPrice(propertyId, checkIn, checkOut);

  // Criar Stripe Payment Intent
  const paymentIntent = await stripe.paymentIntents.create({
    amount: pricing.total,
    currency: 'usd',
    metadata: {
      propertyId,
      checkIn,
      checkOut,
      guestId: user.id,
    },
  });

  // Inserir 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) {
    // A restrição de exclusão vai pegar double bookings
    if (error.code === '23P01') {
      return { error: 'Essas datas foram acabadas de ser reservadas por outra pessoa.' };
    }
    throw error;
  }

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

Note como tratamos o código de erro 23P01 — esse é a violação de exclusão do PostgreSQL. Mesmo que dois usuários cliquem "Reservar" no exato mesmo milissegundo, apenas uma reserva passa.

Busca e Filtragem com PostGIS

Geo-busca é inegociável para plataformas de aluguel. Aqui está uma função Postgres que manipula busca por raio com 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;
$$;

Chame-a a partir do 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',
});

Isso roda em menos de 50ms para 100K+ anúncios com indexação apropriada. Nenhum Elasticsearch necessário até você atingir escala muito maior.

Autenticação e Acesso Multi-Papel

Supabase Auth manipula o trabalho pesado. A parte complicada é a natureza dual-papel das plataformas de aluguel — alguém pode ser tanto um hóspede quanto um host.

Manipulo isso com um campo de role no profile que evolui de guest para host quando criam seu primeiro anúncio, mais políticas Row Level Security:

-- Hosts podem editar apenas suas próprias propriedades
create policy "hosts_manage_own_properties" on public.properties
  for all using (host_id = auth.uid());

-- Hóspedes podem visualizar propriedades listadas
create policy "anyone_view_listed" on public.properties
  for select using (status = 'listed');

-- Hóspedes podem ver apenas suas próprias reservas
create policy "guests_own_bookings" on public.bookings
  for select using (guest_id = auth.uid());

-- Hosts podem ver reservas para suas propriedades
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 é genuinamente uma das features mais fortes do Supabase para apps multi-tenant. As regras de segurança vivem perto dos dados, não espalhadas em middleware de API.

Processamento de Pagamentos e Pagamentos

Use Stripe Connect. Ponto final. Ele manipula pagamentos de marketplace, splits, 1099s, KYC, e pagamentos internacionais. A alternativa é construir seu próprio sistema de divisão de pagamentos, o que é... não faça.

Aqui está o fluxo:

  1. Host se integra via Stripe Connect Express (Stripe manipula a UI de verificação de identidade)
  2. Hóspede paga via Stripe Payment Intents
  3. Pagamento fica retido até check-in + 24 horas
  4. Payout transfere para o host menos sua taxa de plataforma

Preço Stripe Connect em 2025: 0.25% + $0.25 por payout além das taxas de processamento padrão (2.9% + $0.30 por cobrança). Para uma reserva de $200/noite, você está olhando para aproximadamente $6.50 em taxas Stripe. Orce por isso.

Mensagens em Tempo Real e Notificações

Supabase Realtime torna mensagens host-hóspede simples:

// Se inscrever em novas mensagens em uma conversa
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 notificações por email (confirmações de reserva, lembretes de check-in), uso Resend ou SendGrid acionadas a partir de Supabase Edge Functions via webhooks de banco de dados. O preço do Resend começa em $20/mês para 50K emails — mais que suficiente para uma plataforma em crescimento.

Manipulação de Imagens e Performance

Fotos de propriedades fazem ou quebram taxas de conversão. Cada anúncio pode ter 15-30 imagens, e elas precisam carregar rápido.

Minha abordagem:

  • Carregar originais para Supabase Storage
  • Usar a API de transformação de imagem do Supabase para redimensionamento on-the-fly
  • Servir via componente <Image> do Next.js com sizes e srcSet apropriados
  • Lazy-load tudo abaixo da dobra
  • Usar placeholder blur com pequeno preview base64 gerado no momento do upload
<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"
/>

Essa abordagem entrega sub-segundo LCP em páginas de anúncios com 20+ fotos.

Considerações de Implantação e Dimensionamento

Para implantação, Vercel é a escolha natural para Next.js. Mas aqui está uma nuance que a maioria dos artigos pula: use o Edge Runtime do Vercel com moderação para uma plataforma de aluguel. O fluxo de reserva precisa de Node.js runtime para Stripe SDK e operações complexas de banco de dados. Edge é ótimo para middleware (geo-redirects, verificações de auth) mas não para lógica de negócio.

Opção de Implantação Melhor Para Custo Mensal (estimativa)
Vercel Pro + Supabase Pro MVP para 10K MAU $45 - $100
Vercel Pro + Supabase Team 10K-100K MAU $200 - $500
Next.js Auto-hospedado + Supabase Pro Otimização de custo $100 - $300
AWS/GCP + Supabase Auto-hospedado Controle total em escala $500+

Supabase Pro começa em $25/mês por projeto e inclui 8GB de banco de dados, 250GB de largura de banda, e 100GB de armazenamento. Suficiente para a maioria dos MVPs e plataformas no início.

Detalhamento de Custos: O Que Você Realmente Gastará

Aqui está o que uma plataforma de aluguel de temporada real custa para construir e rodar em 2025:

Item Custo MVP Custo Mensal de Operação
Supabase Pro - $25
Vercel Pro - $20
Stripe Connect - ~2.9% + $0.30/transação
Mapbox/Google Maps - $0-200 (baseado em uso)
Resend (email) - $20
Domínio + DNS (Cloudflare) $15/ano $0
Desenvolvimento (agência) $40K-120K -
Desenvolvimento (dev solo) $15K-40K -
Total Mensal de Infra - ~$65-265

Compare com construir em uma plataforma SaaS como Sharetribe ($299-599/mês) ou Guesty, e a economia do desenvolvimento customizado começar a fazer sentido assim que você tem qualquer tração real.

Se você está sério sobre construir uma plataforma de aluguel e quer desenvolvedores experientes que já entregaram esse tipo exato de produto, entre em contato com nossa equipe ou verifique nossa página de preços para estimativas de projeto. Nós especializamos em desenvolvimento de CMS headless e aplicações Next.js complexas.

FAQ

Quanto tempo leva para construir uma plataforma de aluguel de temporada como Airbnb?

Um MVP funcional com anúncios, busca, reservas, pagamentos, e mensagens leva 3-5 meses com um time experiente de 2-3 desenvolvedores. Um desenvolvedor solo pode precisar de 6-9 meses. Isso te leva ao lançamento — não à paridade de features com Airbnb, que tem 15+ anos de desenvolvimento atrás dela. Foque em suas features de nicho primeiro.

Supabase é escalável o suficiente para uma plataforma de aluguel em produção?

Sim, até um ponto. Supabase Pro manipula dezenas de milhares de usuários concorrentes confortavelmente. Seu plano Team ($599/mês) suporta significativamente mais. Instagram rodou em um único servidor PostgreSQL por anos. Seu gargalo será product-market fit muito antes de ser escala de banco de dados. Quando você crescer além do Supabase, seus dados estão em PostgreSQL padrão — migração é direta.

Como você previne double bookings em um sistema de aluguel de temporada?

Use restrições de exclusão PostgreSQL com a extensão btree_gist. Isso enforça no nível do banco de dados que nenhuma duas reservas ativas podem ter intervalos de datas sobrepostos para a mesma propriedade. É o único método confiável — verificações no nível de aplicação têm race conditions. O exemplo de schema acima mostra exatamente como implementar isso.

Devo usar Stripe Connect ou construir meu próprio sistema de pagamento?

Stripe Connect. Sempre. Construir seu próprio sistema de divisão de pagamentos para um marketplace envolve licenças de transmissão de dinheiro, conformidade KYC/AML, relatórios de impostos internacionais, e prevenção de fraude. Stripe manipula tudo isso. As taxas (aproximadamente 3.2% por transação) valem a pena. Você sempre pode negociar taxas uma vez que esteja processando volume significativo.

Qual é a melhor maneira de manipular busca de propriedade com mapas?

PostGIS com Supabase para as queries de backend, e Mapbox GL JS ou Google Maps JavaScript API para o frontend. PostGIS queries espaciais com índices GiST apropriados manipulam buscas de raio e bounding-box em milissegundos. Preço Mapbox começa com tier gratuito generoso (50K carregamentos de mapa/mês). Google Maps cobra $7 por 1000 carregamentos de mapa dinâmicos após o crédito mensal de $200.

Como manipulo preços sazonais e taxas dinâmicas?

Use uma tabela de override de disponibilidade/preço baseada em data junto com o preço base da propriedade. Para cada noite de uma reserva, verifique se há um override de preço para aquele dia específico. Se não, volte para o preço base. Isso manipula taxas sazonais, premiums de fim de semana, preços de feriados, e descontos de última hora. Algumas plataformas também se integram com PriceLabs ($19.99/anúncio/mês) ou Beyond Pricing para preço dinâmico automatizado.

Next.js é melhor que Astro para uma plataforma de aluguel?

Para uma plataforma completa de aluguel com fluxos de reserva interativos, mensagens, e dashboards — Next.js vence. A app precisa de interatividade significativa no lado do cliente. Astro excele em sites pesados em conteúdo com interatividade mínima (verifique nossas capacidades de desenvolvimento Astro). Dito isso, se você está construindo um site apenas de anúncios sem reservas (como um diretório), a performance do Astro seria extraordinária.

E quanto a apps móveis — preciso React Native também?

Não para seu MVP. Construa a app Next.js como um PWA responsivo (Progressive Web App) primeiro. Adicione notificações push, cache offline, e um prompt "Adicionar à Tela Inicial". Isso cobre 80% dos casos de uso móvel. Uma vez que você validou o produto e tem receita real, invista em apps nativas. Muitas plataformas de aluguel de nicho bem-sucedidas (Hipcamp, Glamping Hub) lançaram web-first e adicionaram apps nativas depois.