The 200 Mates application uses Supabase as its backend-as-a-service, providing PostgreSQL database, file storage, and real-time capabilities.
Supabase Setup
Client Initialization
const SUPABASE_URL = "https://qpwwexlxiksmaaehqsev.supabase.co" ;
const SUPABASE_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InFwd3dleGx4aWtzbWFhZWhxc2V2Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzE5ODk5MjEsImV4cCI6MjA4NzU2NTkyMX0.zfrepatjz41WDVIbxp61FblT8lKPyNpU-HB0RElIpgc" ;
const sb = supabase . createClient ( SUPABASE_URL , SUPABASE_ANON );
The SUPABASE_ANON key is intentionally public and safe to expose in client-side code. Row-level security (RLS) policies on the backend restrict access appropriately.
Project Configuration
Setting Value Project ID qpwwexlxiksmaaehqsevRegion Auto-selected Database PostgreSQL 15 Storage Bucket mate-photos (public)Authentication Anonymous access only
Database Schema
mates Table
CREATE TABLE mates (
id BIGSERIAL PRIMARY KEY ,
public_id TEXT ,
name TEXT NOT NULL ,
country TEXT NOT NULL ,
country_code TEXT ,
brand TEXT NOT NULL ,
preparation TEXT NOT NULL ,
mate_type TEXT ,
photo_path TEXT NOT NULL ,
lat DOUBLE PRECISION ,
lng DOUBLE PRECISION ,
approved BOOLEAN DEFAULT NULL ,
created_at TIMESTAMPTZ DEFAULT NOW ()
);
Columns
Column Type Nullable Description idBIGSERIALNo Auto-increment primary key public_idTEXTYes Human-readable public ID nameTEXTNo Submitter’s name countryTEXTNo Country name (localized) country_codeTEXTYes ISO 3166-1 alpha-3 code brandTEXTNo Yerba mate brand preparationTEXTNo Preparation style mate_typeTEXTYes Vessel type (optional) photo_pathTEXTNo Storage filename latDOUBLE PRECISIONYes Latitude lngDOUBLE PRECISIONYes Longitude approvedBOOLEANYes Moderation status created_atTIMESTAMPTZNo Submission timestamp
Indexes
CREATE INDEX idx_mates_approved ON mates(approved);
CREATE INDEX idx_mates_created_at ON mates(created_at DESC );
CREATE INDEX idx_mates_country_code ON mates(country_code);
Indexes on approved and created_at optimize the main query that fetches approved mates in reverse chronological order.
Database Queries
Fetching Approved Mates
The main query retrieves all approved mate submissions:
async function loadMates () {
try {
const { data , error } = await sb
. from ( "mates" )
. select ( "*" )
. not ( "approved" , "eq" , false ) // Exclude rejected mates
. order ( "created_at" , { ascending: false });
if ( error ) throw error ;
allMates = data || [];
// Process data
const matesWithCoords = allMates
. map ( m => {
const c = resolveCoords ( m );
return c ? { ... m , ... c } : null ;
})
. filter ( Boolean );
countriesColored = [ ... new Set ( allMates . map ( m => {
if ( m . country_code ) return m . country_code . toUpperCase (). trim ();
if ( m . country ) return nameToIso3 [ m . country . toLowerCase (). trim ()] || null ;
return null ;
}). filter ( Boolean ))];
renderPolygons ();
renderMarkers ( matesWithCoords );
renderArcs ( matesWithCoords );
renderGallery ();
updateStats ();
} catch ( err ) {
console . error ( "loadMates error:" , err );
}
}
Query Breakdown:
.from("mates") - Query the mates table
.select("*") - Select all columns
.not("approved", "eq", false) - Include NULL (pending) and true (approved), exclude false (rejected)
.order("created_at", { ascending: false }) - Sort newest first
The .not("approved", "eq", false) clause allows both pending (null) and approved (true) mates to display. This provides instant feedback to users while submissions await moderation.
Auto-Refresh Strategy
loadMates ();
setInterval ( loadMates , 30_000 ); // Refresh every 30 seconds
New submissions appear within 30 seconds without requiring a page refresh.
Storage Bucket
mate-photos Bucket
Configuration:
Name : mate-photos
Public : Yes (read-only)
File size limit : 5 MB
Allowed file types : image/*
Uploading Photos
const photo = document . getElementById ( "photo" ). files [ 0 ];
const ext = photo . name . split ( "." ). pop ();
const fileName = ` ${ Date . now () } _ ${ Math . random (). toString ( 36 ). slice ( 2 ) } . ${ ext } ` ;
const { error : ue } = await sb . storage
. from ( "mate-photos" )
. upload ( fileName , photo , {
contentType: photo . type
});
if ( ue ) throw ue ;
Filename Format:
{timestamp}_{random}.{extension}
Example:
1709801234567_k2j8x9q.jpg
The timestamp + random string combination ensures unique filenames and prevents collisions.
Generating Public URLs
function getPhotoUrl ( path ) {
if ( ! path ) return null ;
const { data } = sb . storage . from ( "mate-photos" ). getPublicUrl ( path );
return data ?. publicUrl || null ;
}
Example Output:
https://qpwwexlxiksmaaehqsev.supabase.co/storage/v1/object/public/mate-photos/1709801234567_k2j8x9q.jpg
Inserting New Mates
const { error : ie } = await sb . from ( "mates" ). insert ([{
name ,
country ,
country_code: countryCode || null ,
brand ,
preparation ,
mate_type: mate_type || null ,
photo_path: fileName ,
lat: isNaN ( lat ) ? null : lat ,
lng: isNaN ( lng ) ? null : lng ,
approved: null // Pending moderation
}]);
if ( ie ) throw ie ;
New submissions have approved: null by default. They are visible immediately but should be reviewed by moderators. Consider implementing a moderation dashboard to set approved to true or false.
Row-Level Security (RLS)
Recommended Policies
While the current implementation allows anonymous access, these RLS policies are recommended for production:
Read Policy (Public)
CREATE POLICY "Public read access" ON mates
FOR SELECT
USING (approved IS NOT FALSE); -- Allow NULL and TRUE
Insert Policy (Authenticated)
CREATE POLICY "Authenticated insert" ON mates
FOR INSERT
WITH CHECK ( auth . role () = 'anon' ); -- Allow anonymous inserts
Update Policy (Service Role Only)
CREATE POLICY "Service role update" ON mates
FOR UPDATE
USING ( auth . role () = 'service_role' ); -- Only backend can update
Delete Policy (Service Role Only)
CREATE POLICY "Service role delete" ON mates
FOR DELETE
USING ( auth . role () = 'service_role' ); -- Only backend can delete
Storage Policies
Upload Policy
CREATE POLICY "Public upload" ON storage . objects
FOR INSERT
WITH CHECK (bucket_id = 'mate-photos' );
Read Policy
CREATE POLICY "Public read" ON storage . objects
FOR SELECT
USING (bucket_id = 'mate-photos' );
Error Handling
Database Errors
try {
const { data , error } = await sb . from ( "mates" ). select ( "*" );
if ( error ) throw error ;
// Process data
} catch ( err ) {
console . error ( "Database error:" , err );
alert ( ` ${ t ( "alertError" ) }${ err . message } ` );
}
Common Error Codes
Code Description Solution PGRST301Row not found Check query filters PGRST116Invalid JSON Validate request body 42P01Table does not exist Check table name spelling 23505Unique violation Handle duplicate entries
Storage Errors
const { error } = await sb . storage . from ( "mate-photos" ). upload ( fileName , file );
if ( error ) {
if ( error . statusCode === "413" ) {
alert ( "File too large (max 5MB)" );
} else {
alert ( `Upload failed: ${ error . message } ` );
}
}
Real-Time Subscriptions (Optional)
For instant updates, enable real-time subscriptions:
const channel = sb
. channel ( 'mates-changes' )
. on (
'postgres_changes' ,
{
event: '*' ,
schema: 'public' ,
table: 'mates' ,
filter: 'approved=eq.true'
},
( payload ) => {
console . log ( 'Change received!' , payload );
loadMates (); // Refresh data
}
)
. subscribe ();
Real-time subscriptions require enabling the Realtime feature in your Supabase project settings.
Query Optimization
Create indexes on frequently queried columns (approved, created_at, country_code).
For pagination, use .range(start, end) to limit results: . range ( 0 , 99 ) // First 100 results
Only fetch needed columns to reduce payload size: . select ( 'id, name, country, photo_path, lat, lng' )
Cache generated photo URLs to avoid redundant getPublicUrl() calls.
Connection Pooling
Supabase automatically handles connection pooling. For high-traffic applications, consider:
Enabling connection pooling in Supabase dashboard
Using edge functions for API endpoints
Implementing CDN caching for photos
Local Development
Supabase CLI Setup
For local development with Supabase:
# Install Supabase CLI
npm install -g supabase
# Initialize project
supabase init
# Start local Supabase
supabase start
# Update constants.js with local URL
const SUPABASE_URL = "http://localhost:54321" ;
Seed Data
Create supabase/seed.sql with sample mates:
INSERT INTO mates ( name , country, country_code, brand, preparation, photo_path, lat, lng, approved)
VALUES
( 'Juan Pérez' , 'Argentina' , 'ARG' , 'La Merced' , 'amargo' , 'sample1.jpg' , - 34 . 6037 , - 58 . 3816 , true),
( 'Maria Silva' , 'Brasil' , 'BRA' , 'Barao' , 'chimarrao' , 'sample2.jpg' , - 15 . 8267 , - 47 . 9218 , true),
( 'Carlos González' , 'Uruguay' , 'URY' , 'Canarias' , 'amargo' , 'sample3.jpg' , - 34 . 9011 , - 56 . 1645 , true);
Migration Scripts
Create database migrations in supabase/migrations/:
20260101000000_create_mates_table.sql
CREATE TABLE mates (
id BIGSERIAL PRIMARY KEY ,
public_id TEXT ,
name TEXT NOT NULL ,
country TEXT NOT NULL ,
country_code TEXT ,
brand TEXT NOT NULL ,
preparation TEXT NOT NULL ,
mate_type TEXT ,
photo_path TEXT NOT NULL ,
lat DOUBLE PRECISION ,
lng DOUBLE PRECISION ,
approved BOOLEAN DEFAULT NULL ,
created_at TIMESTAMPTZ DEFAULT NOW ()
);
CREATE INDEX idx_mates_approved ON mates(approved);
CREATE INDEX idx_mates_created_at ON mates(created_at DESC );
CREATE INDEX idx_mates_country_code ON mates(country_code);
Apply Migrations
Next Steps
Data Structures Understand the mate and country data models
Internationalization Learn about the translation system