These docs are a work in progress and may not be fully up to date. Some pages may contain internal notes for our team.
Skip to Content

Data model — Location

A single physical place (store, office, venue) that can appear as a pin on a Finder map and as a row in the results list.

Table: locations

ColumnTypeNullableDescription
idbigint (auto-increment)noPrimary key
titlevarcharnoLocation name shown to visitors
street_address_1varcharyesPrimary address line
street_address_2varcharyesSuite, floor, unit, etc.
street_address_3varcharyesAdditional address line
cityvarcharyesCity
statevarcharyesState or province
postal_codevarcharyesPostcode / ZIP
countryvarcharyesCountry
latitudedecimal(10,7)yesWGS-84 latitude; populated by the geocoding pipeline
longitudedecimal(10,7)yesWGS-84 longitude; populated by the geocoding pipeline
phonevarcharyesContact phone number
websitevarcharyesURL of the location’s website (added in migration 2026_04_17)
image_urlvarcharyesPublic CDN URL of the location hero image
image_keyvarcharyesR2 storage object key used for delete-on-replace; not exposed to visitors
custom_fieldsjsonyesKey-value map of custom field values keyed by CustomFieldDefinition.name. See JSON columns
created_attimestampnoLaravel auto-managed
updated_attimestampnoLaravel auto-managed

Relations

  • belongsToMany Set — via set_location pivot. Sets (called “Categories” in the UI) group locations for filtering. The pivot exposes set_id and location_id. When serialized via toArray(), sets are returned as a flat array of IDs only.
  • belongsToMany Tag — via location_tag pivot. Free-form workspace-scoped labels.
  • belongsToMany Workspace — via location_workspace pivot. A location belongs to one or more workspaces.
  • belongsToMany User — via location_user pivot (owners). Returns only users.id for performance.

Maps access locations through the map_location pivot (on the Map model), not directly from Location.

Geocoding

latitude and longitude are populated from the address fields via the autocomplete provider selected for the finder. The frontend submits coordinates captured from the place-detail lookup at edit time; the backend stores them directly. There is no background geocoding job — coordinates are set at save time if the user selects a place from the autocomplete dropdown.

🔴 [NEEDS CLARIFICATION: If a location is created/imported without coordinates (e.g. via AI import or CSV), what populates lat/lng — is there a batch geocoding step?]

Image storage

image_url is the public CDN URL constructed via publicCdnUrl() helper using services.cdn.base_url. The R2 object key format is:

workspaces/{workspace_id}/locations/{location_id}/{uuid}.{ext}

image_key stores this key so the old object can be deleted when a new image is uploaded. The upload endpoint is POST /locations/{id}/image. See codebases/backend/storage.

JSON columns

custom_fields

A flat key-value map where each key is the name field of a CustomFieldDefinition belonging to the workspace. Values are typed according to the field’s type (text, textarea, number, select, url, email, or image):

{ "hours": "Mon–Fri 9am–5pm", "rating": 4.5, "featured": true, "region": "Northeast" }

The shape is defined by CustomFieldDefinition records on the workspace. Fields not present in the map are treated as empty/null on the frontend.

Computed / virtual attributes

toArray() is overridden to return sets as a flat array of integer IDs rather than the full pivot relation objects:

$array['sets'] = $this->sets->pluck('id')->toArray();

This is the serialization contract used by the API — consumers should expect sets: [1, 2, 3], not an array of Set objects.