Data Layer
Database Schema
PostgreSQL database with Drizzle ORM powering the entire platform. 14 tables organized into logical domains supporting property, vehicle, and notification data.
Location Hierarchy
Normalized geographic data for precise location filtering
provincesSouth African provinces
citiesCities with province reference
neighbourhoodsNeighbourhoods with city reference
Core Data
Primary business entities for listings, sellers, and scraping
sellersSeller registry with verification badges & activity metrics
gumtree_property_listingsProperty listings with extended attributes (JSONB images)
gumtree_vehicle_listingsVehicle listings with make/model/year attributes
scrape_queueQueue for resume support & scrape tracking
listing_mediaMultiple images per listing (DO Spaces storage)
WhatsApp Recipients
User notification preferences and subscription management
whatsapp_recipientsRegistered notification recipients
recipient_quiet_hoursDo-not-disturb schedules per user
recipient_table_subscriptionsCategory subscriptions with quality thresholds
recipient_location_filtersLocation-based notification filters
Notification Tracking
Audit log for all sent notifications
notification_logsComplete notification history with Twilio references
Entity Relationships
provincescitiescitiesneighbourhoodscitieslistingsneighbourhoodslistingssellersproperty_listingssellersvehicle_listingslistingslisting_mediawhatsapp_recipientsquiet_hourswhatsapp_recipientssubscriptionssubscriptionslocation_filterswhatsapp_recipientsnotification_logsDrizzle Schema Definition
// Core Listings Tables
export const gumtreePropertyListings = pgTable('gumtree_property_listings', {
id: serial('id').primaryKey(),
gumtreeId: varchar('gumtree_id', { length: 255 }).unique().notNull(),
title: text('title').notNull(),
price: varchar('price', { length: 100 }),
images: jsonb('images').$type<string[]>().default([]), // JSONB array
bedrooms: integer('bedrooms'),
bathrooms: integer('bathrooms'),
parking: integer('parking'),
petFriendly: boolean('pet_friendly'),
sizeSqm: integer('size_sqm'),
leadQuality: varchar('lead_quality', { length: 20 }).default('bad'),
});
export const gumtreeVehicleListings = pgTable('gumtree_vehicle_listings', {
id: serial('id').primaryKey(),
gumtreeId: varchar('gumtree_id', { length: 255 }).unique().notNull(),
make: varchar('make', { length: 100 }),
model: varchar('model', { length: 100 }),
year: integer('year'),
km: integer('km'),
fuelType: varchar('fuel_type', { length: 50 }),
transmission: varchar('transmission', { length: 50 }),
});
// Sellers with Verification Badges
export const sellers = pgTable('sellers', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 255 }).notNull(),
phone: varchar('phone', { length: 50 }),
idVerified: boolean('id_verified').default(false),
mobileVerified: boolean('mobile_verified').default(false),
emailVerified: boolean('email_verified').default(false),
totalViews: integer('total_views').default(0),
sellingFor: varchar('selling_for', { length: 100 }),
});