This document defines the complete database schema for the BitcoinLatte application using PostgreSQL (Supabase).
Extended user profile information.
CREATE TABLE public.profiles (
id UUID REFERENCES auth.users(id) PRIMARY KEY,
email TEXT,
is_admin BOOLEAN DEFAULT FALSE,
display_name TEXT,
avatar_url TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);Indexes:
idx_profiles_is_adminonis_admin
RLS Policies:
- Public read access
- Users can update their own profile
- Admins can update any profile
Approved coffee shops that accept Bitcoin/crypto.
CREATE TABLE public.shops (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
address TEXT NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
crypto_accepted JSONB DEFAULT '[]'::jsonb,
website TEXT,
phone TEXT,
hours JSONB,
approved BOOLEAN DEFAULT FALSE,
submitted_by UUID REFERENCES auth.users(id),
approved_by UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT valid_latitude CHECK (latitude >= -90 AND latitude <= 90),
CONSTRAINT valid_longitude CHECK (longitude >= -180 AND longitude <= 180)
);crypto_accepted format:
["BTC", "ETH", "LTC", "Lightning"]hours format:
{
"monday": {"open": "08:00", "close": "18:00"},
"tuesday": {"open": "08:00", "close": "18:00"},
"wednesday": {"open": "08:00", "close": "18:00"},
"thursday": {"open": "08:00", "close": "18:00"},
"friday": {"open": "08:00", "close": "18:00"},
"saturday": {"open": "09:00", "close": "17:00"},
"sunday": {"closed": true}
}Indexes:
idx_shops_locationonlatitude, longitude(for geospatial queries)idx_shops_approvedonapprovedidx_shops_created_atoncreated_at
RLS Policies:
- Public read access for approved shops
- Authenticated users can read all shops
- Admins can insert/update/delete
Pending shop submissions awaiting approval.
CREATE TABLE public.submissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
description TEXT,
address TEXT NOT NULL,
latitude DECIMAL(10, 8) NOT NULL,
longitude DECIMAL(11, 8) NOT NULL,
crypto_accepted JSONB DEFAULT '[]'::jsonb,
website TEXT,
phone TEXT,
hours JSONB,
status TEXT DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected')),
submitted_by UUID REFERENCES auth.users(id),
reviewed_by UUID REFERENCES auth.users(id),
review_notes TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
reviewed_at TIMESTAMP WITH TIME ZONE,
CONSTRAINT valid_latitude CHECK (latitude >= -90 AND latitude <= 90),
CONSTRAINT valid_longitude CHECK (longitude >= -180 AND longitude <= 180)
);Indexes:
idx_submissions_statusonstatusidx_submissions_submitted_byonsubmitted_byidx_submissions_created_atoncreated_at
RLS Policies:
- Users can read their own submissions
- Admins can read all submissions
- Anyone can insert (anonymous submissions allowed)
- Admins can update/delete
Images associated with approved shops.
CREATE TABLE public.shop_images (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES public.shops(id) ON DELETE CASCADE NOT NULL,
image_url TEXT NOT NULL,
thumbnail_url TEXT NOT NULL,
is_primary BOOLEAN DEFAULT FALSE,
uploaded_by UUID REFERENCES auth.users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);Indexes:
idx_shop_images_shop_idonshop_ididx_shop_images_is_primaryonis_primary
RLS Policies:
- Public read access
- Authenticated users can insert
- Image owner or admin can delete
Images associated with pending submissions.
CREATE TABLE public.submission_images (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
submission_id UUID REFERENCES public.submissions(id) ON DELETE CASCADE NOT NULL,
image_url TEXT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);Indexes:
idx_submission_images_submission_idonsubmission_id
RLS Policies:
- Users can read images for their submissions
- Admins can read all
- Anyone can insert
- Admins can delete
User comments on shops.
CREATE TABLE public.comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES public.shops(id) ON DELETE CASCADE NOT NULL,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
content TEXT NOT NULL,
comment_type TEXT DEFAULT 'general' CHECK (comment_type IN ('general', 'bitcoin_experience', 'review')),
parent_id UUID REFERENCES public.comments(id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT content_length CHECK (char_length(content) >= 1 AND char_length(content) <= 2000)
);Indexes:
idx_comments_shop_idonshop_ididx_comments_user_idonuser_ididx_comments_parent_idonparent_ididx_comments_created_atoncreated_at
RLS Policies:
- Public read access
- Authenticated users can insert
- Comment owner can update/delete their own
- Admins can delete any
User votes on shops and submissions.
CREATE TABLE public.votes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
shop_id UUID REFERENCES public.shops(id) ON DELETE CASCADE,
submission_id UUID REFERENCES public.submissions(id) ON DELETE CASCADE,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE NOT NULL,
vote_type TEXT NOT NULL CHECK (vote_type IN ('shop_quality', 'bitcoin_verified', 'submission_accuracy')),
value INTEGER NOT NULL CHECK (value IN (-1, 1)),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT vote_target CHECK (
(shop_id IS NOT NULL AND submission_id IS NULL) OR
(shop_id IS NULL AND submission_id IS NOT NULL)
),
CONSTRAINT unique_user_vote UNIQUE (user_id, shop_id, submission_id, vote_type)
);Indexes:
idx_votes_shop_idonshop_ididx_votes_submission_idonsubmission_ididx_votes_user_idonuser_id
RLS Policies:
- Public read access (aggregated)
- Authenticated users can insert/update their own votes
- Users can delete their own votes
Stores shop and submission images.
Configuration:
- Public: false
- File size limit: 5MB
- Allowed MIME types: image/jpeg, image/png, image/webp
Policies:
- Anyone can upload (with rate limiting)
- Public read access
- Owner or admin can delete
Trigger function to automatically update updated_at timestamp.
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;Applied to:
- shops
- profiles
- comments
Calculate aggregate score for a shop based on votes.
CREATE OR REPLACE FUNCTION calculate_shop_score(shop_uuid UUID)
RETURNS TABLE(
quality_score INTEGER,
bitcoin_verified_score INTEGER,
total_votes INTEGER
) AS $$
BEGIN
RETURN QUERY
SELECT
COALESCE(SUM(CASE WHEN vote_type = 'shop_quality' THEN value ELSE 0 END), 0)::INTEGER,
COALESCE(SUM(CASE WHEN vote_type = 'bitcoin_verified' THEN value ELSE 0 END), 0)::INTEGER,
COUNT(*)::INTEGER
FROM votes
WHERE shop_id = shop_uuid;
END;
$$ LANGUAGE plpgsql;Get shops within a radius of a location.
CREATE OR REPLACE FUNCTION get_nearby_shops(
lat DECIMAL,
lng DECIMAL,
radius_km DECIMAL DEFAULT 10
)
RETURNS TABLE(
id UUID,
name TEXT,
address TEXT,
latitude DECIMAL,
longitude DECIMAL,
distance_km DECIMAL
) AS $$
BEGIN
RETURN QUERY
SELECT
s.id,
s.name,
s.address,
s.latitude,
s.longitude,
(
6371 * acos(
cos(radians(lat)) * cos(radians(s.latitude)) *
cos(radians(s.longitude) - radians(lng)) +
sin(radians(lat)) * sin(radians(s.latitude))
)
)::DECIMAL AS distance_km
FROM shops s
WHERE s.approved = TRUE
HAVING distance_km <= radius_km
ORDER BY distance_km;
END;
$$ LANGUAGE plpgsql;-- Auto-update updated_at on shops
CREATE TRIGGER update_shops_updated_at
BEFORE UPDATE ON shops
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Auto-update updated_at on profiles
CREATE TRIGGER update_profiles_updated_at
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- Auto-update updated_at on comments
CREATE TRIGGER update_comments_updated_at
BEFORE UPDATE ON comments
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();-- Enable RLS
ALTER TABLE profiles ENABLE ROW LEVEL SECURITY;
-- Public read
CREATE POLICY "Public profiles are viewable by everyone"
ON profiles FOR SELECT
USING (true);
-- Users can update own profile
CREATE POLICY "Users can update own profile"
ON profiles FOR UPDATE
USING (auth.uid() = id);
-- Admins can update any profile
CREATE POLICY "Admins can update any profile"
ON profiles FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);-- Enable RLS
ALTER TABLE shops ENABLE ROW LEVEL SECURITY;
-- Public can view approved shops
CREATE POLICY "Approved shops are viewable by everyone"
ON shops FOR SELECT
USING (approved = true);
-- Authenticated users can view all shops
CREATE POLICY "Authenticated users can view all shops"
ON shops FOR SELECT
USING (auth.role() = 'authenticated');
-- Admins can insert shops
CREATE POLICY "Admins can insert shops"
ON shops FOR INSERT
WITH CHECK (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);
-- Admins can update shops
CREATE POLICY "Admins can update shops"
ON shops FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);
-- Admins can delete shops
CREATE POLICY "Admins can delete shops"
ON shops FOR DELETE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);-- Enable RLS
ALTER TABLE submissions ENABLE ROW LEVEL SECURITY;
-- Anyone can insert submissions
CREATE POLICY "Anyone can submit shops"
ON submissions FOR INSERT
WITH CHECK (true);
-- Users can view their own submissions
CREATE POLICY "Users can view own submissions"
ON submissions FOR SELECT
USING (submitted_by = auth.uid());
-- Admins can view all submissions
CREATE POLICY "Admins can view all submissions"
ON submissions FOR SELECT
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);
-- Admins can update submissions
CREATE POLICY "Admins can update submissions"
ON submissions FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);-- Enable RLS
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
-- Public can view comments
CREATE POLICY "Comments are viewable by everyone"
ON comments FOR SELECT
USING (true);
-- Authenticated users can insert comments
CREATE POLICY "Authenticated users can comment"
ON comments FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update own comments
CREATE POLICY "Users can update own comments"
ON comments FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete own comments
CREATE POLICY "Users can delete own comments"
ON comments FOR DELETE
USING (auth.uid() = user_id);
-- Admins can delete any comment
CREATE POLICY "Admins can delete any comment"
ON comments FOR DELETE
USING (
EXISTS (
SELECT 1 FROM profiles
WHERE id = auth.uid() AND is_admin = true
)
);-- Enable RLS
ALTER TABLE votes ENABLE ROW LEVEL SECURITY;
-- Public can view aggregated votes
CREATE POLICY "Vote counts are viewable by everyone"
ON votes FOR SELECT
USING (true);
-- Authenticated users can vote
CREATE POLICY "Authenticated users can vote"
ON votes FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can update own votes
CREATE POLICY "Users can update own votes"
ON votes FOR UPDATE
USING (auth.uid() = user_id);
-- Users can delete own votes
CREATE POLICY "Users can delete own votes"
ON votes FOR DELETE
USING (auth.uid() = user_id);Common cryptocurrency types to support:
- BTC (Bitcoin)
- Lightning (Lightning Network)
- ETH (Ethereum)
- USDC (USD Coin)
- USDT (Tether)
- LTC (Litecoin)
- BCH (Bitcoin Cash)
- DOGE (Dogecoin)
- Create profiles table
- Create shops table
- Create submissions table
- Create shop_images table
- Create submission_images table
- Create comments table
- Create votes table
- Create functions
- Create triggers
- Enable RLS and create policies
- Create storage buckets and policies
Additional composite indexes for common queries:
-- Shops by location and crypto type
CREATE INDEX idx_shops_location_crypto ON shops
USING GIN (crypto_accepted);
-- Recent comments
CREATE INDEX idx_comments_shop_recent ON comments (shop_id, created_at DESC);
-- Vote aggregation
CREATE INDEX idx_votes_aggregation ON votes (shop_id, vote_type, value);- Daily automated backups via Supabase
- Point-in-time recovery enabled
- Weekly manual backup verification
- Monthly VACUUM ANALYZE on all tables
- Quarterly index rebuild
- Regular monitoring of table sizes and query performance