A full-stack equipment rental platform built with Next.js 14, TypeScript, Supabase, and Tailwind CSS.
| Layer | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript 5 |
| Database + Auth | Supabase (PostgreSQL + Row Level Security) |
| Storage | Supabase Storage (equipment-images bucket) |
| Styling | Tailwind CSS v4 + inline CSS-in-JS |
| Resend | |
| Deployment | Vercel |
git clone https://github.com/your-org/construction-rental.git
cd construction-rental
npm installcp .env.example .env.localFill in .env.local with your real values (see table below).
npm run devOpen http://localhost:3000.
| Variable | Required | Description |
|---|---|---|
NEXT_PUBLIC_SUPABASE_URL |
✅ | Your Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
✅ | Supabase anonymous/public key |
RESEND_API_KEY |
✅ | Resend API key for booking emails |
NEXT_PUBLIC_WHATSAPP_NUMBER |
Optional | WhatsApp number without + (e.g. 919876543210) |
NEXT_PUBLIC_CALL_NUMBER |
Optional | Call number with + (e.g. +919876543210) |
NEXT_PUBLIC_ADMIN_EMAIL |
Optional | Contact email shown in footer |
NEXT_PUBLIC_SITE_URL |
Optional | Canonical URL for sitemap/OG tags |
create table equipment (
id uuid primary key default gen_random_uuid(),
name text not null,
description text,
category text not null, -- 'excavators' | 'cranes' | 'forklifts' | 'compactors' | 'telehandlers' | 'compressors'
daily_rate numeric not null,
image_url text,
images text[] default '{}',
is_available boolean default true,
created_at timestamptz default now()
);create table bookings (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users,
equipment_id uuid references equipment(id),
customer_name text,
customer_email text,
customer_phone text,
equipment_name text,
start_date date,
end_date date,
total_amount numeric default 0,
status text default 'pending', -- 'pending' | 'confirmed' | 'completed' | 'cancelled'
notes text,
created_at timestamptz default now()
);create table profiles (
id uuid primary key references auth.users on delete cascade,
email text,
full_name text,
phone text,
role text default 'user', -- 'user' | 'admin'
created_at timestamptz default now()
);create table reviews (
id uuid primary key default gen_random_uuid(),
user_id uuid references auth.users not null,
equipment_id uuid references equipment(id) not null,
rating int check (rating between 1 and 5),
comment text,
created_at timestamptz default now(),
unique (user_id, equipment_id)
);Create a public bucket named equipment-images.
Add a storage policy that allows authenticated users to upload:
-- INSERT policy for authenticated users
create policy "Authenticated users can upload"
on storage.objects for insert
to authenticated
with check (bucket_id = 'equipment-images');
-- SELECT policy for public read
create policy "Public read"
on storage.objects for select
to public
using (bucket_id = 'equipment-images');Enable RLS on all tables. Recommended policies:
-- Equipment: public read
alter table equipment enable row level security;
create policy "Public read equipment" on equipment for select to public using (true);
create policy "Admin full access equipment" on equipment for all to authenticated
using ((select role from profiles where id = auth.uid()) = 'admin');
-- Bookings: users see their own
alter table bookings enable row level security;
create policy "Users see own bookings" on bookings for select to authenticated
using (user_id = auth.uid() or customer_email = (select email from profiles where id = auth.uid()));
create policy "Anyone can insert booking" on bookings for insert to public with check (true);
create policy "Admin full access bookings" on bookings for all to authenticated
using ((select role from profiles where id = auth.uid()) = 'admin');src/
├── app/
│ ├── admin/ # Admin panel (role-protected)
│ ├── catalog/ # Equipment catalog + detail pages
│ ├── dashboard/ # User booking dashboard
│ ├── login/ # Auth pages
│ └── api/ # API routes (email, quote)
├── components/
│ ├── layout/ # Navbar, Footer
│ └── ui/ # Reusable UI components
├── hooks/
│ └── useUser.ts # Auth state hook
├── lib/
│ ├── supabase.ts # Supabase client (browser + server)
│ ├── constants.ts # App-wide constants (phone, email, categories)
│ └── env.ts # Environment variable validation
├── types/
│ └── index.ts # Shared TypeScript interfaces
└── middleware.ts # Route protection (SSR-correct Supabase session check)
- Create a user via Supabase Auth (email/password).
- In the
profilestable, setrole = 'admin'for that user's row. - Navigate to
/admin— middleware validates auth session and admin role before route access.
vercel --prodSet all environment variables in the Vercel dashboard under Project → Settings → Environment Variables.
Add the Supabase project URL to the allowedOrigins in your Supabase dashboard under Authentication → URL Configuration.