Files
exposedgays/supabase/schema.sql
2026-06-26 21:32:17 +00:00

246 lines
9.8 KiB
PL/PgSQL

-- ExposedGays Supabase Schema
-- Run in Supabase SQL Editor or via CLI: supabase db push
-- Enable PostGIS for map geolocation
CREATE EXTENSION IF NOT EXISTS postgis;
-- ============================================================
-- PROFILES
-- ============================================================
CREATE TABLE public.profiles (
id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE,
username TEXT UNIQUE,
display_name TEXT,
age INTEGER CHECK (age >= 18 AND age <= 120),
position TEXT CHECK (position IN ('top', 'bottom', 'vers', 'side')),
bio TEXT,
avatar_url TEXT,
status TEXT NOT NULL DEFAULT 'offline'
CHECK (status IN ('online', 'looking', 'hosting', 'traveling', 'offline')),
is_anonymous BOOLEAN NOT NULL DEFAULT true,
kinks TEXT[] DEFAULT '{}',
sti_status TEXT,
on_prep BOOLEAN DEFAULT false,
last_active TIMESTAMPTZ NOT NULL DEFAULT now(),
location GEOGRAPHY(POINT, 4326),
city TEXT,
vanilla_mode BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_profiles_location ON public.profiles USING GIST (location);
CREATE INDEX idx_profiles_status ON public.profiles (status);
CREATE INDEX idx_profiles_city ON public.profiles (city);
CREATE INDEX idx_profiles_last_active ON public.profiles (last_active DESC);
-- ============================================================
-- CRUISING SPOTS
-- ============================================================
CREATE TABLE public.cruising_spots (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
category TEXT NOT NULL
CHECK (category IN ('park', 'beach', 'gym', 'restroom', 'bookstore', 'club', 'other')),
location GEOGRAPHY(POINT, 4326) NOT NULL,
city TEXT NOT NULL,
rating NUMERIC(2,1) DEFAULT 0,
active_count INTEGER DEFAULT 0,
is_popular BOOLEAN DEFAULT false,
created_by UUID REFERENCES public.profiles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_spots_location ON public.cruising_spots USING GIST (location);
CREATE INDEX idx_spots_city ON public.cruising_spots (city);
CREATE INDEX idx_spots_category ON public.cruising_spots (category);
-- ============================================================
-- CHAT ROOMS
-- ============================================================
CREATE TABLE public.chat_rooms (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('city', 'spot', 'group', 'dm')),
city TEXT,
spot_id UUID REFERENCES public.cruising_spots(id) ON DELETE CASCADE,
created_by UUID REFERENCES public.profiles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_rooms_type ON public.chat_rooms (type);
CREATE INDEX idx_rooms_city ON public.chat_rooms (city);
-- Room membership
CREATE TABLE public.chat_room_members (
room_id UUID REFERENCES public.chat_rooms(id) ON DELETE CASCADE,
user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE,
joined_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (room_id, user_id)
);
-- ============================================================
-- CHAT MESSAGES
-- ============================================================
CREATE TABLE public.chat_messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
room_id UUID NOT NULL REFERENCES public.chat_rooms(id) ON DELETE CASCADE,
sender_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
content TEXT NOT NULL,
image_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_messages_room ON public.chat_messages (room_id, created_at DESC);
CREATE INDEX idx_messages_sender ON public.chat_messages (sender_id);
-- Enable Realtime
ALTER PUBLICATION supabase_realtime ADD TABLE public.chat_messages;
ALTER PUBLICATION supabase_realtime ADD TABLE public.profiles;
-- ============================================================
-- PHOTO GALLERY
-- ============================================================
CREATE TABLE public.gallery_photos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES public.profiles(id) ON DELETE CASCADE,
url TEXT NOT NULL,
is_explicit BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX idx_gallery_user ON public.gallery_photos (user_id);
-- ============================================================
-- SHOP PRODUCTS (optional DB-backed; static JSON also works)
-- ============================================================
CREATE TABLE public.shop_products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
slug TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
description TEXT,
price NUMERIC(10,2) NOT NULL DEFAULT 0,
category TEXT NOT NULL,
image_url TEXT,
affiliate_url TEXT NOT NULL,
affiliate_label TEXT NOT NULL DEFAULT 'Buy Now',
kinks TEXT[] DEFAULT '{}',
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- ============================================================
-- HELPER: Update location from lat/lng
-- ============================================================
CREATE OR REPLACE FUNCTION public.update_profile_location(
p_user_id UUID,
p_lat DOUBLE PRECISION,
p_lng DOUBLE PRECISION
) RETURNS VOID AS $$
BEGIN
UPDATE public.profiles
SET location = ST_SetSRID(ST_MakePoint(p_lng, p_lat), 4326)::geography,
last_active = now(),
updated_at = now()
WHERE id = p_user_id;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Auto-create profile on signup
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO public.profiles (id, username, is_anonymous)
VALUES (NEW.id, NEW.email, true);
RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE TRIGGER on_auth_user_created
AFTER INSERT ON auth.users
FOR EACH ROW EXECUTE FUNCTION public.handle_new_user();
-- ============================================================
-- ROW LEVEL SECURITY
-- ============================================================
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.cruising_spots ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.chat_rooms ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.chat_room_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.chat_messages ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.gallery_photos ENABLE ROW LEVEL SECURITY;
ALTER TABLE public.shop_products ENABLE ROW LEVEL SECURITY;
-- Profiles: anyone can read, owners can update
CREATE POLICY "Profiles are viewable by everyone"
ON public.profiles FOR SELECT USING (true);
CREATE POLICY "Users can update own profile"
ON public.profiles FOR UPDATE USING (auth.uid() = id);
CREATE POLICY "Users can insert own profile"
ON public.profiles FOR INSERT WITH CHECK (auth.uid() = id);
-- Spots: public read, authenticated create
CREATE POLICY "Spots are viewable by everyone"
ON public.cruising_spots FOR SELECT USING (true);
CREATE POLICY "Authenticated users can create spots"
ON public.cruising_spots FOR INSERT
WITH CHECK (auth.role() = 'authenticated');
-- Chat rooms: members can read
CREATE POLICY "Chat rooms viewable by authenticated users"
ON public.chat_rooms FOR SELECT
USING (auth.role() = 'authenticated');
CREATE POLICY "Authenticated users can create rooms"
ON public.chat_rooms FOR INSERT
WITH CHECK (auth.role() = 'authenticated');
-- Chat messages: room members can read/write
CREATE POLICY "Messages viewable by authenticated users"
ON public.chat_messages FOR SELECT
USING (auth.role() = 'authenticated');
CREATE POLICY "Authenticated users can send messages"
ON public.chat_messages FOR INSERT
WITH CHECK (auth.uid() = sender_id);
-- Gallery: public read for non-anonymous, owner write
CREATE POLICY "Gallery photos viewable by everyone"
ON public.gallery_photos FOR SELECT USING (true);
CREATE POLICY "Users can manage own gallery"
ON public.gallery_photos FOR ALL USING (auth.uid() = user_id);
-- Shop products: public read
CREATE POLICY "Shop products are public"
ON public.shop_products FOR SELECT USING (is_active = true);
-- ============================================================
-- SEED: Fort Lauderdale city chat room
-- ============================================================
INSERT INTO public.chat_rooms (name, type, city)
VALUES ('Fort Lauderdale Chat', 'city', 'Fort Lauderdale');
-- SEED: Sample cruising spots
INSERT INTO public.cruising_spots (name, description, category, location, city, rating, active_count, is_popular)
VALUES
('Hugh Taylor Birch State Park', 'Popular trails behind the nature center.', 'park',
ST_SetSRID(ST_MakePoint(-80.1048, 26.1452), 4326)::geography, 'Fort Lauderdale', 4.2, 8, true),
('Sebastian Street Beach', 'Gay beach section. Day and night action.', 'beach',
ST_SetSRID(ST_MakePoint(-80.1042, 26.1175), 4326)::geography, 'Fort Lauderdale', 4.5, 12, true),
('Triple Crown Bookstore', 'Adult bookstore with viewing booths.', 'bookstore',
ST_SetSRID(ST_MakePoint(-80.1534, 26.1912), 4326)::geography, 'Oakland Park', 4.0, 6, true);
-- SEED: Shop products
INSERT INTO public.shop_products (slug, name, description, price, category, image_url, affiliate_url, affiliate_label, kinks)
VALUES
('pistol-pete-jock', 'Pistol Pete Jockstrap', 'Premium mesh jock with contoured pouch.', 24.99, 'jocks-harness',
'https://placehold.co/400x400/1a1220/ff2d6b?text=Pistol+Pete+Jock', 'https://www.adammale.com', 'Buy on AdamMale', '{underwear,cruising}'),
('rush-poppers', 'Rush Poppers', 'Classic amyl nitrite formula. 10ml bottle.', 18.99, 'poppers-lube',
'https://placehold.co/400x400/1a1220/8b2fc9?text=Rush+Poppers', 'https://www.adammale.com', 'Buy on AdamMale', '{pnp}'),
('10in-dildo', '10" Realistic Dildo', 'Veined silicone, suction cup base.', 49.99, 'dildos-plugs',
'https://placehold.co/400x400/1a1220/ff2d6b?text=10in+Dildo', 'https://www.adammale.com', 'Buy on AdamMale', '{anal}');