mlsgrid-sync

module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 3, 2026 License: Apache-2.0

README

mlsgrid-sync

CI Release Go Report Card Go Reference License

Replicate MLS Grid listing feeds into PostgreSQL — correctly, resumably, and inside the rate limits.

Status: v0.1. The full replication lifecycle — backfill, incremental sync, reconcile, media download, field scopes — is implemented, tested against synthetic fixtures, and validated against a live MRED feed. Pre-1.0 caveats: the schema contract is stable (v1.0.0), but CLI flags and config keys may still shift (roadmap).

mlsgrid-sync is a single Go binary that performs the full MLS Grid replication lifecycle against the RESO Web API (OData):

  • Backfill — resumable initial import (MlgCanView eq true), surviving interrupts and the API's $skip pagination wall
  • Incremental sync — cursor-based catch-up (--once for cron, --daemon for continuous), with hard deletion of records the feed revokes
  • Reconcile — periodic full-feed sweep that purges deletions missed while the tool wasn't running
  • Media — metadata-only mode, or download-to-your-own-storage (local disk or any S3-compatible endpoint), as MLS Grid rules require
  • Field scopes — keep everything (full), a curated set (standard, analytics), or the bare minimum (minimal); stable core columns + a raw JSONB column mean scope changes never require schema migrations

The database schema is a versioned, documented contract — see docs/schema-contract.md — designed to be queried directly or through the companion MCP server, mlsgrid-mcp.

Compliance — read this first

This tool ships no MLS data and no credentials. To use it you must:

  1. Have an executed MLS Grid Data License Agreement and per-MLS approval for every feed (OriginatingSystemName) you replicate.
  2. Follow your license tier's display rules (IDX/VOW/back-office) on anything you build with the data.
  3. Self-host any media you display — MLS Grid media URLs are for downloading only; hot-linking is prohibited.

The tool automatically enforces what it can: it deletes revoked records (MlgCanView=false) as the license requires, runs below the published rate limits (and refuses config that exceeds them), backs off on HTTP 429, and sends the required User-Agent token header on media downloads. Details in docs/compliance.md.

Quickstart

With Docker (bundles PostgreSQL):

cp config.example.yaml mlsgrid-sync.yaml   # edit: your feed's system slug
export MLSGRID_TOKEN_MRED=...              # your MLS Grid bearer token
docker compose run --rm sync init-db       # create schema + run migrations
docker compose run --rm sync backfill      # initial import (resumable)
docker compose up -d sync                  # continuous replication

Or against your own PostgreSQL — install a release binary or go install github.com/piotrsenkow/mlsgrid-sync/cmd/mlsgrid-sync@latest:

cp config.example.yaml mlsgrid-sync.yaml   # edit: your feed's system slug + token env var
export MLSGRID_DATABASE_URL=postgres://... # or database.url in the config
mlsgrid-sync init-db
mlsgrid-sync backfill
mlsgrid-sync sync --daemon                 # deploy/mlsgrid-sync.service for systemd

Trying it out on a token you also use elsewhere? Bound the trial: backfill --since 24h --max-pages 5 imports a day's slice in a handful of requests, and incremental sync takes over from there.

Commands

Command Purpose
init-db Create the mlsgrid schema and run migrations
backfill Initial full import; --force to redo, --no-expand for a fast column-only pass
sync --once / --daemon Incremental replication from the persisted cursor
reconcile Full-feed key sweep; purges records deleted while offline
media download Drain queued photos to the configured sink; --max-files bounds a run
media retry Re-queue failed media downloads
status Cursors, record counts, media queue, rate-budget usage

Field scopes

Core columns (see the schema contract) are always populated. The profile's field_scope controls everything beyond them — how much of each record survives into the raw JSONB column, which child expansions are requested, and whether requests are narrowed with $select:

Scope raw keeps Notes
minimal nothing (NULL) skips Rooms/UnitTypes expansions and auto-narrows requests with $select — the cheapest scope against the hourly byte budget
standard (default) a curated keep-list: remarks, participants, dates/prices, structure, interior/exterior, systems, HOA, schools, rental terms drops vendor internals and the long tail
analytics standard + tax, income, expenses, and MLS-local (MRD_*) fields for investment analysis
full the record byte-for-byte as received lossless, largest
path/to/scope.yaml your own include:/exclude: glob lists exclude beats include; empty include means everything

Changing scope never requires a schema migration, and already-stored rows are not rewritten — the scope applies to records as they arrive.

Media

The default mode (metadata-only) stores media rows — URLs, captions, dimensions — without fetching files. Setting media.mode: download in a profile queues every discovered photo (storage_status='pending') and media download drains the queue into a sink: a local directory or any S3-compatible bucket (AWS, MinIO, Cloudflare R2 — S3 credentials come from the standard AWS environment variables).

Rules the downloader enforces for you:

  • Every request sends User-Agent: <your access token> — mandatory for MLS Grid media since 2026-06-01.
  • Downloads count against the same hourly byte budget as the feed, so a big backlog just proceeds at your configured limits. Use --max-files to bound a run when the token is shared.
  • MediaKeys are immutable, so a downloaded key is never fetched twice; replaced photos arrive as new keys.
  • A dead URL never blocks the queue: it is retried on later runs and parked as failed after 3 attempts (media retry re-queues).
  • When a revoked listing is hard-deleted, its downloaded files are removed from the sink too.

Design notes

Built from production experience running MLS Grid replication at scale (MRED / Chicagoland). The design decisions that matter — a dedicated sync_state cursor table that refuses to run with an empty watermark, ge cursor semantics with idempotent upserts, wall-clock-aligned persisted rate budgets, a circuit breaker on repeated 429s, MediaKey-immutability-based dedup — are documented in docs/architecture.md (including an appendix of the production incidents this design encodes) and enforced by test fixtures.

Found a feed quirk this tool mishandles? Open an MLS quirk report — that's how alias maps and unmarshalers grow beyond MRED.

License

Apache-2.0. Not affiliated with or endorsed by MLS Grid LLC.

Directories

Path Synopsis
cmd
mlsgrid-sync command
internal
cli
Package cli defines the mlsgrid-sync command tree.
Package cli defines the mlsgrid-sync command tree.
config
Package config loads and validates mlsgrid-sync configuration from a YAML file with MLSGRID_-prefixed environment overrides.
Package config loads and validates mlsgrid-sync configuration from a YAML file with MLSGRID_-prefixed environment overrides.
engine
Package engine orchestrates replication: it drives the mlsgrid Pager against the store, owning cursor semantics (backfill here; incremental sync and reconcile arrive in later milestones).
Package engine orchestrates replication: it drives the mlsgrid Pager against the store, owning cursor semantics (backfill here; incremental sync and reconcile arrive in later milestones).
fieldscope
Package fieldscope handles per-MLS field-name normalization and (from M8) field-scope presets.
Package fieldscope handles per-MLS field-name normalization and (from M8) field-scope presets.
media
Package media downloads listing photos queued by the sync engine (storage_status='pending') into a local-disk or S3-compatible sink.
Package media downloads listing photos queued by the sync engine (storage_status='pending') into a local-disk or S3-compatible sink.
mlsgrid
Package mlsgrid implements a client for the MLS Grid API (RESO Web API / OData v4): request/paging mechanics, quirk-tolerant record decoding, and retry orchestration against the rate limiter.
Package mlsgrid implements a client for the MLS Grid API (RESO Web API / OData v4): request/paging mechanics, quirk-tolerant record decoding, and retry orchestration against the rate limiter.
ratelimit
Package ratelimit enforces the MLS Grid usage limits client-side: a per-second token bucket, hourly/daily request windows and an hourly byte budget aligned to UTC wall-clock boundaries, and a circuit breaker that halts the process rather than hammering a suspended token.
Package ratelimit enforces the MLS Grid usage limits client-side: a per-second token bucket, hourly/daily request windows and an hourly byte budget aligned to UTC wall-clock boundaries, and a circuit breaker that halts the process rather than hammering a suspended token.
store
Package store defines the persistence interface the sync engine writes through.
Package store defines the persistence interface the sync engine writes through.
store/postgres
Package postgres implements store.Store on PostgreSQL via pgx/v5.
Package postgres implements store.Store on PostgreSQL via pgx/v5.
testclock
Package testclock provides a deterministic Clock for tests: Sleep advances virtual time instantly and records every requested duration.
Package testclock provides a deterministic Clock for tests: Sleep advances virtual time instantly and records every requested duration.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL