Skip to main content
This page documents all data structures used in the 200 Mates application, including database schemas, GeoJSON formats, and configuration objects.

Mate Object

The core data structure representing a user submission.

Database Schema

interface Mate {
  id: number;              // Auto-increment primary key
  public_id?: string;      // Human-readable public ID (optional)
  name: string;            // Submitter's name
  country: string;         // Country name (localized)
  country_code?: string;   // ISO 3166-1 alpha-3 code (e.g., "ARG")
  brand: string;           // Yerba mate brand name
  preparation: string;     // "amargo" | "dulce" | "tereré" | "Mate Cocido"
  mate_type?: string;      // Vessel type (e.g., "Calabaza", "Madera")
  photo_path: string;      // Filename in Supabase storage bucket
  lat?: number;            // Latitude (GPS or capital fallback)
  lng?: number;            // Longitude (GPS or capital fallback)
  approved: boolean | null;// true = approved, false = rejected, null = pending
  created_at: string;      // ISO 8601 timestamp
}

Example Object

{
  "id": 42,
  "public_id": "200M-ARG-042",
  "name": "Juan Pérez",
  "country": "Argentina",
  "country_code": "ARG",
  "brand": "La Merced",
  "preparation": "amargo",
  "mate_type": "Calabaza",
  "photo_path": "1709801234567_abc123.jpg",
  "lat": -34.6037,
  "lng": -58.3816,
  "approved": true,
  "created_at": "2026-03-07T14:32:15.000Z"
}
If GPS coordinates are not available (lat/lng are null), the resolveCoords() function in utils.js falls back to the capital city coordinates for the given country.

Country Data

Country Object

interface Country {
  name: string;      // English name
  nameEs: string;    // Spanish name
  namePt: string;    // Portuguese name
  iso3: string;      // ISO 3166-1 alpha-3 code
  flag: string;      // Unicode flag emoji
}

Example

countries.js:12
{ 
  name: "Argentina", 
  nameEs: "Argentina", 
  namePt: "Argentina", 
  iso3: "ARG", 
  flag: "🇦🇷" 
}
The COUNTRIES array in data/countries.js contains 202 country objects.

ISO Tables

Two lookup tables enable conversion between country identifiers:
iso-tables.js
// Numeric ID → ISO3 (for GeoJSON)
const numericToIso3 = {
  "32": "ARG",
  "76": "BRA",
  "152": "CHL",
  "600": "PRY",
  "858": "URY",
  // ...
};

// Country name (lowercase) → ISO3
const nameToIso3 = {
  "argentina": "ARG",
  "brazil": "BRA",
  "brasil": "BRA",
  "chile": "CHL",
  "paraguay": "PRY",
  "uruguay": "URY",
  // ...
};

Capital Coordinates

capital-coords.js
const capitalCoords = {
  "ARG": { lat: -34.6037, lng: -58.3816 },  // Buenos Aires
  "BRA": { lat: -15.8267, lng: -47.9218 },  // Brasília
  "CHL": { lat: -33.4489, lng: -70.6693 },  // Santiago
  "PRY": { lat: -25.2637, lng: -57.5759 },  // Asunción
  "URY": { lat: -34.9011, lng: -56.1645 },  // Montevideo
  // ...
};
Capital coordinates are used as fallback locations when GPS data is unavailable. This ensures all mates can be displayed on the globe.

GeoJSON Format

Country Feature

Country boundaries are stored as GeoJSON features:
interface CountryFeature {
  type: "Feature";
  id: string;           // Numeric country ID
  properties: object;   // Metadata (unused in 200 Mates)
  geometry: {
    type: "Polygon" | "MultiPolygon";
    coordinates: number[][][] | number[][][][];
  };
  iso3?: string;        // Added by app for lookup
}

Example

{
  "type": "Feature",
  "id": "32",
  "properties": { "name": "Argentina" },
  "geometry": {
    "type": "MultiPolygon",
    "coordinates": [
      [[[-65.5, -55.2], [-66.4, -55.2], ...]],
      [[[-68.6, -52.6], ...]]
    ]
  },
  "iso3": "ARG"
}

Data Source

GeoJSON is generated from World Atlas TopoJSON:
app.js:59
fetch("https://unpkg.com/world-atlas@2/countries-110m.json")
  .then(r => r.ok ? r.json() : Promise.reject())
  .then(world => {
    const raw = topojson.feature(world, world.objects.countries).features;
    countriesGeo = raw.map(f => ({
      ...f, 
      iso3: numericToIso3[String(parseInt(f.id, 10))] || null
    }));
    renderPolygons();
  });

Translation Structure

i18n Object

interface Translation {
  [language: string]: {
    [key: string]: string;
  };
}

const i18n: Translation = {
  es: { ... },  // Spanish
  en: { ... },  // English
  pt: { ... }   // Portuguese
};

Example Keys

i18n.js:5
const i18n = {
  es: {
    title: "Vuelta al Mundo en unos 200 Mates",
    statMates: "Mates",
    statCountries: "Países",
    formTitle: "Cebate uno",
    placeholderName: "Nombre*",
    placeholderCountry: "País*",
    placeholderYerba: "Marca de yerba*",
    bitter: "Amargo",
    sweet: "Dulce",
    terere: "Tereré",
    brewedMateTea: "Mate Cocido",
    submitBtn: "Enviar",
    // ... 100+ keys
  },
  en: { ... },
  pt: { ... }
};

Translation Categories

CategoryKeysDescription
Headertitle, statMates, statCountriesSite title and stats
GlobespinHint, recentMatesGlobe UI text
FormformTitle, placeholderName, submitBtnSubmission form
Preparationbitter, sweet, terere, brewedMateTeaMate styles
AlertsalertRequired, alertSuccess, alertErrorValidation messages
ModerationmodText1 - modText7Community guidelines
FAQfaq1q, faq1a - faq3q, faq3aFrequently asked questions
Privacypriv1text - priv8textPrivacy policy
Termsterms1title - terms8textTerms of service

State Variables

Global application state in modules/state.js:
state.js
let allMates: Mate[] = [];              // All approved mates from database
let countriesColored: string[] = [];    // ISO3 codes of countries with mates
let countriesGeo: CountryFeature[] = [];// GeoJSON features for globe rendering
let arcInterval: number | null = null;  // setInterval ID for arc animation
let polyTimer: number | null = null;    // setTimeout ID for polygon debounce
let lastMarkerIds: string = "";         // Cache key for marker rendering
let currentLang: string = "es";         // Active language code

Supabase Response Types

Query Response

interface SupabaseQueryResponse {
  data: Mate[] | null;
  error: {
    message: string;
    details: string;
    hint: string;
    code: string;
  } | null;
}

Storage Upload Response

interface SupabaseStorageResponse {
  data: {
    path: string;
  } | null;
  error: {
    message: string;
    statusCode: string;
  } | null;
}

Example Query

app.js:18
const { data, error } = await sb
  .from("mates")
  .select("*")
  .not("approved", "eq", false)
  .order("created_at", { ascending: false });

if (error) throw error;
allMates = data || [];

Form Data

FormData Structure

Data captured from the submission form:
interface FormSubmission {
  name: string;          // User's name
  country: string;       // Country (with flag emoji prefix)
  countryCode: string;   // ISO3 code
  brand: string;         // Yerba brand
  preparation: string;   // Preparation type
  mate_type: string;     // Vessel type (optional)
  photo: File;           // Photo file
  lat: number | null;    // GPS latitude
  lng: number | null;    // GPS longitude
}

Validation Rules

FieldRequiredConstraints
nameYesNon-empty string
countryYesMust match country list
brandYesNon-empty string
preparationYesOne of: “amargo”, “dulce”, “tereré”, “Mate Cocido”
mate_typeNoFree text
photoYesImage file (JPG, PNG), max 5MB
lat/lngNoValid coordinates or null

Constants

Configuration Values

config/constants.js
const SUPABASE_URL = "https://qpwwexlxiksmaaehqsev.supabase.co";
const SUPABASE_ANON = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
const sb = supabase.createClient(SUPABASE_URL, SUPABASE_ANON);

const DEFAULT_ALTITUDE = 2.5;  // Default globe camera altitude
const ZOOM_THRESHOLD = 1.8;    // Altitude threshold for zoom detection
The SUPABASE_ANON key is intentionally public and has row-level security (RLS) policies enabled on the Supabase backend.

Next Steps

Supabase Integration

Learn about the backend setup and queries

Internationalization

Understand the translation system

Build docs developers (and LLMs) love