A production-ready marketplace platform for South Africa's pet-sitting industry
Live Site: siteasy.co.za | Code Samples: github.com/selezai/siteasy-public
Built with Next.js 14, TypeScript, Supabase, and Paystack
SitEasy is a full-stack marketplace connecting pet owners with verified pet sitters in South Africa. The platform handles the complete booking lifecycle from discovery to payment, with real-time messaging, meet & greet scheduling, and comprehensive safety features.
Market Context:
- South Africa's pet sitting market: R50.8M (2024), projected R85.3M by 2030
- 14.1% CAGR - fastest growing segment in pet services
- No dominant local digital marketplace - clear first-mover opportunity
Development Timeline: December 2025 - March 2026 (5 sprints)
Frontend
- Next.js 14 (App Router)
- React 19
- TypeScript
- Tailwind CSS 4
- Lucide Icons
Backend
- Next.js API Routes
- Supabase (PostgreSQL)
- Supabase Auth
- Supabase Storage
- Supabase Realtime
Integrations
- Paystack (Payment Processing)
- Resend (Transactional Email)
- Upstash Redis (Rate Limiting)
- Sentry (Error Monitoring)
- Vercel Analytics
Testing & DevOps
- Playwright (E2E Testing)
- Vercel (Deployment)
- GitHub (Version Control)
15 Core Tables:
profiles- User accounts with role-based accesssitter_profiles- Sitter-specific data (services, rates, verification)client_profiles- Client home details and verificationagencies- Agency managementpets- Pet profiles with medical notesbookings- Booking lifecycle managementmeet_greets- Meet & greet schedulingmessages- Real-time messagingconversations- Message threadingtransactions- Payment trackingreviews- Rating systemnotifications- User notificationscheck_ins- Daily check-in reportspet_care_notes- Care activity logsincident_reports- Safety incident tracking
Key Database Features:
- Row-Level Security (RLS) policies on all tables
- Exclusion constraints for booking overlap prevention
- Stored procedures for complex operations
- Automatic timestamp triggers
- Enum types for type safety
Three distinct user roles with separate dashboards:
- Clients - Pet owners booking services
- Sitters - Service providers
- Agency Admins - Managing multiple sitters
Implementation Highlights:
- Supabase Auth with email/password
- Role-based route protection via middleware
- Profile completion tracking
- Document verification workflow
Search & Filtering:
- Service type (pet sitting, house sitting)
- Pet type compatibility
- Rate range
- Location-based search
- Rating filter
Booking Flow:
- Client selects sitter and dates
- System calculates pricing (platform fee: 15%)
- Client proposes meet & greet time slots
- Sitter accepts/declines booking
- Meet & greet scheduled and confirmed
- Deposit payment (50%) required
- Booking confirmed
- Final payment on completion
Technical Implementation:
// Booking overlap prevention using database exclusion constraint
CREATE EXTENSION IF NOT EXISTS btree_gist;
ALTER TABLE bookings
ADD CONSTRAINT no_overlapping_bookings
EXCLUDE USING gist (
sitter_id WITH =,
daterange(start_date, end_date, '[]') WITH &&
) WHERE (status NOT IN ('cancelled', 'completed'));Features:
- Conversation threading per booking
- Real-time message delivery via Supabase Realtime
- Unread message indicators
- Message history
Implementation:
// Supabase Realtime subscription
const channel = supabase
.channel('messages')
.on(
'postgres_changes',
{
event: 'INSERT',
schema: 'public',
table: 'messages',
filter: `conversation_id=eq.${conversationId}`
},
(payload) => {
setMessages(prev => [...prev, payload.new]);
}
)
.subscribe();Payment Flow:
- Deposit payment (50%) on booking confirmation
- Final payment (50%) on completion
- Platform fee deduction (15%)
- Sitter payout to bank account
- Refund processing for cancellations
Cancellation Fee Structure:
- 7+ days before: Full refund
- 3-6 days before: 50% refund
- 1-2 days before: 25% refund
- <24 hours: No refund
Security Features:
- Webhook signature verification
- Amount validation on payment verification
- Double-processing prevention
- Idempotent transaction handling
- Rate limiting on payment endpoints
Implementation Example:
// Payment verification with amount validation
export async function POST(request: Request) {
const { reference } = await validateRequestBody(request, paymentVerifySchema);
// Verify with Paystack
const response = await verifyPayment(reference);
if (response.data.status !== 'success') {
return NextResponse.json({ error: 'Payment failed' }, { status: 400 });
}
// Validate amount matches expected
if (response.data.amount !== transaction.amount) {
console.error('Amount mismatch detected');
return NextResponse.json({ error: 'Payment amount mismatch' }, { status: 400 });
}
// Update booking status atomically
const { error } = await supabase
.from('transactions')
.update({ status: 'completed' })
.eq('reference', reference)
.eq('status', 'pending'); // Prevent double-processing
// ... rest of logic
}Workflow:
- Client proposes 3 time slots during booking
- Sitter selects preferred slot
- System sends confirmation to both parties
- Automated reminders (24h and 2h before)
- Check-in confirmation on the day
Cron Job Implementation:
// Automated reminders via Vercel Cron
// /api/cron/meet-greet-reminders
export async function GET(request: Request) {
// Verify cron secret
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const today = new Date().toLocaleDateString('en-CA', {
timeZone: 'Africa/Johannesburg'
});
// Find meet & greets needing reminders
const { data: meetGreets } = await supabase
.from('meet_greets')
.select('*, bookings(*), profiles(*)')
.eq('date', today)
.eq('status', 'confirmed')
.is('24_hour_reminder_sent', false);
// Send reminders via email
for (const mg of meetGreets) {
await sendEmail({
to: mg.profiles.email,
subject: 'Meet & Greet Reminder - Tomorrow',
// ... email content
});
}
}Features:
- Bidirectional reviews (client reviews sitter, sitter reviews client)
- 5-star rating system
- Optional written feedback
- Average rating calculation
- Review gating (only after booking completion)
Rating Calculation:
// Atomic rating update using database function
CREATE OR REPLACE FUNCTION update_sitter_rating(sitter_uuid UUID)
RETURNS void AS $$
BEGIN
UPDATE sitter_profiles
SET
rating_average = (
SELECT COALESCE(AVG(rating), 0)
FROM reviews
WHERE reviewee_id = sitter_uuid
),
rating_count = (
SELECT COUNT(*)
FROM reviews
WHERE reviewee_id = sitter_uuid
)
WHERE user_id = sitter_uuid;
END;
$$ LANGUAGE plpgsql;Features:
- Sitters submit daily check-in reports during active bookings
- Photo uploads of pets
- Care activity logging (feeding, walks, medications)
- Clients receive real-time updates
All critical findings resolved:
-
RLS Policy Hardening
- Fixed profiles INSERT policy to prevent user impersonation
- Verified all 15 tables have appropriate policies
- Service-role-only tables properly isolated
-
Payment Security
- Amount validation on all payment verifications
- Webhook signature verification using timing-safe comparison
- Double-processing prevention via conditional updates
- Idempotent transaction handling
-
Input Validation
- Zod schemas on all API routes
- Type-safe request/response handling
- SQL injection prevention via parameterized queries
-
Rate Limiting
- Upstash Redis for distributed rate limiting
- Endpoint-specific limits (payments: 10/min, API: 100/min)
- IP-based and user-based identification
-
Authentication & Authorization
- All API routes verify authenticated user
- Ownership checks on all mutations
- Role-based access control
- No client-side database mutations
-
Timezone Handling
- Explicit South Africa timezone (UTC+02:00) handling
- Consistent date parsing to prevent edge cases
- Documented patterns for all date operations
Security Review Results:
- 2 Critical issues: Fixed
- 5 Warning issues: Fixed
- 4 Info items: Documented
- Build verification: Passed
import { z } from 'zod';
export const createBookingSchema = z.object({
sitterId: z.string().uuid('Invalid sitter ID'),
serviceType: z.enum(['pet_sitting', 'drop_in_visits']),
startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format'),
endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Invalid date format'),
notes: z.string().max(1000).optional(),
});
// Usage in API route
export async function POST(request: Request) {
const validation = await validateRequestBody(request, createBookingSchema);
if (!validation.success) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
const { sitterId, serviceType, startDate, endDate } = validation.data;
// ... proceed with validated data
}import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL,
token: process.env.UPSTASH_REDIS_REST_TOKEN,
});
export const rateLimiters = {
payment: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(10, '1 m'),
prefix: 'rl:payment'
}),
api: new Ratelimit({
redis,
limiter: Ratelimit.slidingWindow(100, '1 m'),
prefix: 'rl:api'
}),
};
// Usage
export async function POST(request: Request) {
const identifier = getClientIdentifier(request, userId);
const { success, remaining, resetIn } = await checkRateLimit(identifier, 'payment');
if (!success) {
return rateLimitResponse(resetIn);
}
// ... proceed with request
}export async function POST(request: Request) {
try {
// Auth check
const { data: { user }, error: authError } = await supabase.auth.getUser();
if (authError || !user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
// Rate limiting
const { success } = await checkRateLimit(user.id, 'api');
if (!success) {
return NextResponse.json({ error: 'Too many requests' }, { status: 429 });
}
// Input validation
const validation = await validateRequestBody(request, schema);
if (!validation.success) {
return NextResponse.json({ error: validation.error }, { status: 400 });
}
// Authorization check
const { data: booking } = await supabase
.from('bookings')
.select('client_id')
.eq('id', bookingId)
.single();
if (booking.client_id !== user.id) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
// Business logic
// ...
return NextResponse.json({ success: true });
} catch (error) {
console.error('API Error:', error);
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}Test Coverage:
- Authentication flows (signup, login, password reset)
- Booking creation and cancellation
- Payment processing
- Meet & greet scheduling
- Messaging functionality
- Profile updates
Example Test:
test('complete booking flow', async ({ page }) => {
// Login as client
await page.goto('/login');
await page.fill('[name="email"]', 'client@test.com');
await page.fill('[name="password"]', 'password');
await page.click('button[type="submit"]');
// Search for sitter
await page.goto('/search');
await page.click('[data-testid="sitter-card"]:first-child');
// Create booking
await page.click('[data-testid="book-now"]');
await page.fill('[name="startDate"]', '2026-05-01');
await page.fill('[name="endDate"]', '2026-05-05');
await page.click('[data-testid="submit-booking"]');
// Verify booking created
await expect(page.locator('[data-testid="booking-success"]')).toBeVisible();
});-
Database Indexing
- Composite indexes on frequently queried columns
- GiST index for booking overlap exclusion
- Partial indexes for active records
-
Caching Strategy
- Static page generation for landing pages
- Dynamic rendering for authenticated routes
- Supabase query caching
-
Image Optimization
- Next.js Image component for automatic optimization
- Supabase Storage with CDN
- WebP format support
-
Code Splitting
- Route-based code splitting via App Router
- Dynamic imports for heavy components
- Lazy loading for images
Hosting: Vercel (Production)
Environment Variables:
# Supabase
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# Paystack
PAYSTACK_SECRET_KEY=
PAYSTACK_PUBLIC_KEY=
# Upstash Redis
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
# Email
RESEND_API_KEY=
# Monitoring
SENTRY_DSN=
# Cron
CRON_SECRET=Automated Workflows:
- Booking status updates (daily cron)
- Meet & greet reminders (daily cron)
- Health check monitoring (5-minute intervals)
Challenge: Date-only strings parsed as UTC caused booking status bugs.
Solution: Explicit timezone conversion for all date operations:
// Server-side (cron jobs)
const today = new Date().toLocaleDateString('en-CA', {
timeZone: 'Africa/Johannesburg'
});
// Date parsing for comparisons
const startDate = new Date(booking.start_date + 'T00:00:00');
const endDate = new Date(booking.end_date + 'T23:59:59');Challenge: Webhook and callback could both process the same payment.
Solution: Conditional updates with status checks:
const { error } = await supabase
.from('transactions')
.update({ status: 'completed' })
.eq('reference', reference)
.eq('status', 'pending'); // Only update if still pendingChallenge: Prevent double-booking sitters.
Solution: Database-level exclusion constraint:
EXCLUDE USING gist (
sitter_id WITH =,
daterange(start_date, end_date, '[]') WITH &&
) WHERE (status NOT IN ('cancelled', 'completed'));Challenge: Keep messaging UI in sync across devices.
Solution: Supabase Realtime subscriptions with optimistic updates:
// Optimistic update
setMessages(prev => [...prev, newMessage]);
// Send to server
const { error } = await supabase.from('messages').insert(newMessage);
// Realtime subscription handles sync across devicesDevelopment:
- 5 sprints over 3 months
- 27/27 tasks completed
- 15 database tables
- 40+ API routes
- 50+ React components
Code Quality:
- TypeScript strict mode enabled
- Zero ESLint errors
- All security findings resolved
- Comprehensive E2E test coverage
Performance:
- Lighthouse score: 95+ (desktop)
- First Contentful Paint: <1.5s
- Time to Interactive: <2.5s
Full-Stack Development:
- Next.js 14 App Router architecture
- Server Components and Server Actions
- API route design and implementation
- Database schema design and optimization
AI & Automation:
- Automated workflow scheduling
- Email automation
- Cron job implementation
Payment Integration:
- Paystack API integration
- Webhook handling
- Transaction management
- Refund processing
Security:
- Authentication and authorization
- Row-Level Security policies
- Input validation and sanitization
- Rate limiting and DDoS prevention
- Security audit and remediation
Database:
- PostgreSQL schema design
- Complex queries and joins
- Stored procedures
- Exclusion constraints
- Performance optimization
DevOps:
- Vercel deployment
- Environment management
- Error monitoring (Sentry)
- Cron job scheduling
siteasy/
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── (auth)/ # Auth routes
│ │ ├── (dashboard)/ # Sitter dashboard
│ │ ├── (client)/ # Client dashboard
│ │ ├── (agency)/ # Agency dashboard
│ │ └── api/ # API routes
│ ├── components/ # React components
│ ├── lib/ # Utilities and helpers
│ │ ├── supabase/ # Supabase clients
│ │ ├── paystack.ts # Payment integration
│ │ ├── validation.ts # Zod schemas
│ │ └── rateLimit.ts # Rate limiting
│ └── middleware.ts # Auth middleware
├── supabase/
│ └── migrations/ # Database migrations
├── public/ # Static assets
├── tests/ # Playwright tests
└── docs/ # Documentation
├── IMPLEMENTATION.md # Sprint tracker
├── SECURITY_REVIEW.md # Security audit
└── MARKET_RESEARCH.md # Market analysis
SitEasy demonstrates production-ready full-stack development with a focus on security, scalability, and user experience. The project showcases expertise in modern web technologies, payment integration, real-time features, and comprehensive security practices.
This case study represents:
- 3 months of focused development
- Production-grade code quality
- Security-first architecture
- Real-world business application
- Market research and product strategy
Note: This is a case study of a private business venture. The actual codebase remains private to protect competitive advantage. This documentation showcases the technical architecture, implementation patterns, and engineering decisions without revealing proprietary business logic.
For employment verification or technical discussions, please contact via GitHub.