Construa uma Plataforma de Aluguel de Temporada com Next.js e Supabase
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
- 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-Papel
- Processamento de Pagamentos e Pagamentos
- Mensagens em Tempo Real e Notificações
- Manipulação de Imagens e Performance
- Considerações de Implantação e Dimensionamento
- Detalhamento de Custos: O Que Você Realmente Gastará
- FAQ

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;

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:
- Host se integra via Stripe Connect Express (Stripe manipula a UI de verificação de identidade)
- Hóspede paga via Stripe Payment Intents
- Pagamento fica retido até check-in + 24 horas
- 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 comsizesesrcSetapropriados - Lazy-load tudo abaixo da dobra
- Usar placeholder
blurcom 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.