Construa uma Plataforma de Aluguel de Férias com Next.js e Supabase
Seu visitante passa pela propriedade #47 quando o calendário de disponibilidade congela por três segundos. Ele fecha a aba. Pronto, você perdeu $180 em comissão — e isso aconteceu porque sua tabela de reservas travou durante uma conversão de fuso horário. Eu vi isso acontecer em duas plataformas de aluguel de férias ao vivo, ambas construídas com Next.js e Supabase, ambas processando pagamentos reais via Stripe Connect. Uma atingiu 3.000 listagens em oito meses. A outra quase colapsou com apenas 400 porque o fundador pulou row-level security e deixou hóspedes verem o histórico de reservas um do outro. A diferença não era talento ou orçamento — eram decisões de schema de banco de dados feitas na segunda semana. Aqui está a arquitetura que sobreviveu, os triggers do Supabase que evitaram overbooking às 23h de um sábado, e por que esse stack lida com a complexidade de aluguel de curta duração melhor do que o monolito Rails que todos ainda recomendam.
O mercado de aluguel de férias deve atingir $113,9 bilhões em 2027 (Statista). Airbnb controla uma fatia massiva, mas plataformas de nicho — estadias pet-friendly, vilas de luxo, retiros rurais, surf houses — estão prosperando porque servem públicos específicos melhor do que um generalist jamais conseguiria. Você não precisa vencer o Airbnb. Você precisa servi-los melhor em seu nicho.
Índice
- Por que Next.js + Supabase para uma Plataforma de Aluguel
- Visão Geral da Arquitetura do Sistema
- Design do Schema do Banco de Dados
- Construindo o Motor de Reservas
- Busca e Filtragem com PostGIS
- Autenticação e Acesso Multi-função
- Processamento de Pagamentos e Payouts
- Mensagens em Tempo Real e Notificações
- Manipulação de Imagens e Performance
- Considerações de Deployment e Escalabilidade
- Detalhamento de Custos: O Que Você Realmente Gastará
- FAQ

Por que Next.js + Supabase para uma Plataforma de Aluguel
Seja direto: você poderia construir isso com dezenas de stacks diferentes. Laravel, Rails, Django — todas opções válidas. Mas a combinação Next.js + Supabase atinge um ponto ideal especificamente para plataformas de aluguel.
Next.js oferece:
- Server-side rendering para SEO (páginas de listagem precisam rankear)
- App Router com React Server Components para carregamentos iniciais rápidos
- Rotas de API para lógica backend sem servidor separado
- Otimização de imagens integrada (crítico para fotos de propriedades)
- Incremental Static Regeneration para páginas de listagem que raramente mudam
Supabase oferece:
- PostgreSQL com PostGIS para queries geográficas ("mostre-me aluguéis em um raio de 20km")
- Row Level Security (RLS) que funciona de verdade para apps multi-tenant
- Subscrições em tempo real para mensagens e atualizações de reservas
- Autenticação integrada com provedores OAuth
- Storage para imagens de propriedades com entrega via CDN
- Edge Functions para lógica de negócio serverless
O recurso verdadeiramente mata é que Supabase é apenas Postgres sob o capô. Quando você crescer além da oferta gerenciada do Supabase (ou precisar), você pode migrar para qualquer host Postgres. Sem vendor lock-in em seu ativo mais crítico — seus dados.
Se você está avaliando frameworks, nosso time de desenvolvimento Next.js enviou várias plataformas nesse exato stack.
Visão Geral da Arquitetura do Sistema
Aqui está a arquitetura de alto nível que funcionou bem em vários 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 arquitetural 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 rotas da API do Next.js para tarefas mais leves como queries de busca e validação de formulários. Essa separação importa quando um webhook do Stripe dispara e você precisa garantir que o estado da reserva atualize atomicamente.
Design do Schema do Banco de Dados
Este é o lugar 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:
-- Habilitar 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 payouts de host
identity_verified boolean default false,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Properties/Listagens
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ços
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,
-- Metadata
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 overbooking no nível do DB
exclude using gist (
property_id with =,
daterange(check_in, check_out) with &&
) where (status in ('pending', 'confirmed'))
);
Esse constraint exclude na tabela de reservas? Essa é a linha mais importante de todo o schema. Ela previne overbooking no nível do banco de dados usando um constraint de exclusão GiST. Sem race conditions. Sem emails "desculpe, 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;

Construindo o Motor de Reservas
O fluxo de reserva é o coração de qualquer plataforma de aluguel. Aqui está como eu 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: 'Some dates are unavailable' };
}
// Verificar reservas existentes (cinto + suspensório com constraint 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: 'Dates already booked' };
}
return { available: true };
}
Passo 2: Cálculo de Preço
Nunca confie em cálculos de preço do lado do cliente. Sempre recompute 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('Property not found');
// Obter qualquer override de preço para estas 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
Este é o lugar onde Stripe entra em cena. Eu 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('Not authenticated');
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 lado do 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) {
// O constraint de exclusão vai pegar overbooking
if (error.code === '23P01') {
return { error: 'Estas datas foram reservadas por alguém mais.' };
}
throw error;
}
return {
bookingId: booking.id,
clientSecret: paymentIntent.client_secret,
};
}
Note como nós tratamos o código de erro 23P01 — esse é o erro de violação de exclusão do PostgreSQL. Mesmo se dois usuários clicarem "Reservar" no exato mesmo milissegundo, apenas uma reserva passa.
Busca e Filtragem com PostGIS
Busca por geo-localização é inegociável para plataformas de aluguel. Aqui está uma função Postgres que lida com busca baseada em 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 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: '2026-08-01',
p_check_out: '2026-08-07',
});
Isso roda em menos de 50ms para 100K+ listagens com indexação apropriada. Nenhum Elasticsearch necessário até você atingir escala muito maior.
Autenticação e Acesso Multi-função
Supabase Auth lida com o trabalho pesado. A parte complicada é a natureza dual-função de plataformas de aluguel — alguém pode ser tanto um hóspede quanto um host.
Eu trato isso com um campo role no profile que evolui de guest para host quando eles criam seu primeiro aluguel, mais políticas de 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 ver 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 um dos recursos mais fortes do Supabase para apps multi-tenant. As regras de segurança vivem próximas aos dados, não espalhadas por middleware de API.
Processamento de Pagamentos e Payouts
Use Stripe Connect. Sem exceções. Ele lida com pagamentos de marketplace, splits, 1099s, KYC, e payouts internacionais. A alternativa é construir seu próprio sistema de transmissão de dinheiro, que é... não faça isso.
Aqui está o fluxo:
- Host faz onboarding via Stripe Connect Express (Stripe lida com a UI de verificação de identidade)
- Hóspede paga via Stripe Payment Intents
- Pagamento é retido até check-in + 24 horas
- Payout transfere para host menos sua taxa de plataforma
Precificação do Stripe Connect em 2026: 0.25% + $0.25 por payout em cima das taxas padrão de processamento (2.9% + $0.30 por transação). Para uma reserva de $200/noite, você está procurando em torno de $6.50 em taxas do Stripe. Orçamento para isso.
Mensagens em Tempo Real e Notificações
Supabase Realtime torna a mensagem host-hóspede direta:
// Subscrever a 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), eu uso Resend ou SendGrid acionados por Supabase Edge Functions via webhooks de banco de dados. A precificaçã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 taxa de conversão. Cada listagem pode ter 15-30 imagens, e elas precisam carregar rápido.
Minha abordagem:
- Fazer upload de 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 comsizesesrcSetapropriados - Lazy-load tudo abaixo da dobra
- Usar placeholder
blurcom um preview em base64 tiny gerado no tempo de 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"
/>
Esta abordagem oferece LCP sub-segundo em páginas de listagem com 20+ fotos.
Considerações de Deployment e Escalabilidade
Para deployment, Vercel é a escolha natural para Next.js. Mas aqui está uma nuance que a maioria dos artigos pula: use o Vercel Edge Runtime raramente para uma plataforma de aluguel. O fluxo de reserva precisa do Node.js runtime para SDK do Stripe 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 Deployment | Melhor Para | Custo Mensal (estimativa) |
|---|---|---|
| Vercel Pro + Supabase Pro | MVP até 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 banda, e 100GB de armazenamento. Suficiente para a maioria das MVPs e plataformas em estágio inicial.
Detalhamento de Custos: O Que Você Realmente Gastará
Aqui está o que uma verdadeira plataforma de aluguel de férias custa para construir e executar em 2026:
| Item | Custo MVP | Custo Mensal de Execuçã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 isso com construir em uma plataforma SaaS como Sharetribe ($299-599/mês) ou Guesty, e a economia de desenvolvimento customizado começa a fazer sentido quando você tem qualquer tração real.
Se você está sério em construir uma plataforma de aluguel e quer desenvolvedores experientes que já enviaram exatamente este tipo de produto, entre em contato com nosso time ou veja nossa página de precificação para estimativas de projeto. Nos especializamos em desenvolvimento de headless CMS e aplicações Next.js complexas.
FAQ
Quanto tempo leva para construir uma plataforma de aluguel de férias como Airbnb?
Uma MVP funcional com listagens, 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 recursos com Airbnb, que tem 15+ anos de desenvolvimento. Foque em seus recursos de nicho primeiro.
Supabase é escalável o suficiente para uma plataforma de aluguel em produção?
Sim, até um ponto. Supabase Pro lida com dezenas de milhares de usuários simultâneos confortavelmente. Seu plano Team ($599/mês) suporta significativamente mais. Instagram rodava em um único servidor PostgreSQL por anos. Seu bottleneck será product-market fit muito antes de ser escala de banco de dados. Quando você realmente crescer além do Supabase, seus dados estão em PostgreSQL padrão — migração é direta.
Como você previne overbooking em um sistema de aluguel de férias?
Use constraints de exclusão do PostgreSQL com a extensão btree_gist. Isso força no nível do banco de dados que não há dois bookings ativos que possam ter intervalos de datas sobrepostos para a mesma propriedade. É o único método confiável — verificações em 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, reporte de imposto internacional, e prevenção de fraude. Stripe lida com tudo isso. As taxas (aproximadamente 3.2% por transação) valem a pena. Você sempre pode negociar taxas uma vez que está processando volume significativo.
Qual é a melhor forma de lidar com busca de propriedade com mapas?
PostGIS com Supabase para as queries de backend, e Mapbox GL JS ou JavaScript API do Google Maps para o frontend. Queries espaciais do PostGIS com índices GiST apropriados lidam com buscas de raio e bounding-box em milissegundos. Precificação do Mapbox começa com um tier gratuito generoso (50K map loads/mês). Google Maps cobra $7 por 1000 dynamic map loads após o crédito mensal de $200.
Como eu lido com preços sazonais e taxas dinâmicas?
Use uma tabela de disponibilidade/override de preço baseada em data ao lado do 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 houver, volte ao preço base. Isso lida com taxas sazonais, premiums de fim de semana, preços de feriados, e descontos de última hora. Algumas plataformas também integram com PriceLabs ($19.99/listagem/mês) ou Beyond Pricing para precificação dinâmica automatizada.
Next.js é melhor que Astro para uma plataforma de aluguel?
Para uma plataforma de aluguel completa com fluxos de reserva interativos, mensagens, e dashboards — Next.js vence. O app precisa de significativa interatividade do lado do cliente. Astro se destaca em sites pesados em conteúdo com interatividade mínima (veja nossas capacidades de desenvolvimento Astro). Dito isso, se você está construindo um site de listagens apenas sem reservas (como um diretório), a performance do Astro seria excelente.
E quanto a apps mobile — preciso de React Native também?
Não para sua MVP. Construa o app Next.js como um PWA responsivo (Progressive Web App) primeiro. Adicione notificações push, cache offline, e um prompt "Adicionar à Tela de Início". Isso cobre 80% dos casos de uso móvel. Uma vez que você tenha validado o produto e tenha receita real, invista em apps nativos. Muitas plataformas de aluguel de nicho bem-sucedidas (Hipcamp, Glamping Hub) lançaram web-first e adicionaram apps nativos depois.