OpenDiveMap

Schema

Dive Site Data Model

Each dive site is stored as a row with typed, indexed columns for the fields you filter on most (location, environment, depth, level) and a flexible tags JSONB field for everything else (wildlife, conditions, season). This hybrid approach gives you fast queries without rigid migrations.

Site classification uses two independent dimensions: environment (where the dive happens — ocean, lake, river, etc.) and topologies (terrain features — reef, wall, wreck, etc.). A site has exactly one environment but can have multiple topologies.

All measurements use SI / metric units as the canonical format, following international diving standards (PADI, SSI). Field names encode the unit: _m for meters, _c for Celsius. Client applications handle imperial conversion for display (meters × 3.28084 = feet).

Fields

id UUID auto-generated

Primary key. Generated server-side via gen_random_uuid().

name TEXT required

Primary name of the dive site in its most common form.

latitude DOUBLE PRECISION required

Decimal degrees, WGS 84. Range: -90 to 90.

longitude DOUBLE PRECISION required

Decimal degrees, WGS 84. Range: -180 to 180.

country_code CHAR(2) optional

ISO 3166-1 alpha-2 country code. Examples: MX, ID, BZ.

environment TEXT required

Where the dive happens. Exactly one value per site. Default: ocean.

oceanlakeriverspringquarryfjordpool
topologies TEXT[] required

Terrain features of the dive site. One or more values per site. Stored in a separate junction table, returned as a JSON array in the API.

reefwallpinnaclewreckcavecavernblue holemuckkelp forestchannelartificial reefopen water
max_depth_m INTEGER optional

Maximum depth in meters. The _m suffix is part of the field name by convention.

level TEXT required

Minimum recommended certification level. Default: intermediate.

beginnerintermediateadvanced
tags JSONB optional

Flexible key-value store for additional metadata. See tags reference below.

created_at TIMESTAMPTZ auto

Set automatically on insert.

updated_at TIMESTAMPTZ auto

Updated automatically on every edit via trigger.

Tags reference

The tags field accepts any valid JSON object. The following keys are conventionally used and understood by the API and frontend.

wildlife string[]

Species identifiers, snake_case. E.g. hammerhead_shark, manta_ray.

best_season string[]

Three-letter lowercase months. E.g. ["jul", "aug", "sep"].

current string

Typical current strength.

nonemildmoderatestrong
entry string

How you enter the water.

shoreboatzodiacliveaboard
visibility_avg_m number

Average visibility in meters.

region string

Administrative or geographic region. E.g. Baja California Sur.

sea string

Body of water. E.g. Sea of Cortez, Caribbean Sea.

name_es / name_en / ... string

Localized names. Use ISO 639-1 suffixes.

GeoJSON output

The API returns dive sites as GeoJSON (RFC 7946). A single site from GET /sites/:id returns a Feature:

Feature
{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [longitude, latitude]
  },
  "properties": {
    "id": "uuid",
    "name": "string",
    "country_code": "XX",
    "environment": "ocean | lake | river | ...",
    "topologies": ["reef", "wall", ...],
    "max_depth_m": 0,
    "level": "beginner | intermediate | advanced",
    "tags": { ... },
    "created_at": "ISO 8601",
    "updated_at": "ISO 8601"
  }
}

GET /sites returns a FeatureCollection wrapping an array of Features in the same shape. Supports filters: ?country=, ?level=, ?environment=, ?topology=, ?bbox=west,south,east,north.

Example

A real record from the database — El Bajo Seamount, Sea of Cortez:

El Bajo Seamount
{
  "type": "Feature",
  "geometry": {
    "type": "Point",
    "coordinates": [-110.3981, 24.2156]
  },
  "properties": {
    "id": "dccaf1e1-5df6-4058-a10c-00cdc85e87fb",
    "name": "El Bajo Seamount",
    "country_code": "MX",
    "environment": "ocean",
    "topologies": ["pinnacle"],
    "max_depth_m": 40,
    "level": "advanced",
    "tags": {
      "name_es": "El Bajo",
      "wildlife": ["hammerhead_shark", "manta_ray", "whale_shark"],
      "best_season": ["jul", "aug", "sep", "oct", "nov"],
      "current": "strong",
      "entry": "boat",
      "visibility_avg_m": 20,
      "region": "Baja California Sur",
      "sea": "Sea of Cortez"
    }
  }
}

Version history

Every update to a dive site automatically snapshots the previous version into dive_sites_history via a database trigger. Retrieve past versions with GET /sites/:id/history, which returns an array ordered by most recent change first.