← back to Homesonspec
docs/architecture/ADRS.md
63 lines
# Architecture Decision Records
## ADR-001 — Prisma 6 + plain lat/lon, PostGIS deferred
Prisma 6.19.3 (fleet precedent: govarbitrage). Coordinates are `Decimal(9,6)` with btree
indexes; bbox filters use `lat/lon BETWEEN`, distance sort uses haversine in `$queryRaw`.
All geo math funnels through `packages/shared/src/geo.ts`, so adopting PostGIS later is
one module + one migration. Avoids Prisma's `Unsupported("geography")` friction at a
scale (hundreds of homes per metro) where it buys nothing. Only extension required:
`pg_trgm` (location text resolution).
## ADR-002 — pnpm workspaces, no turborepo
Two Next.js apps + workers + small packages don't justify turbo's config surface. Shared
packages are pure-TS source (`main: ./src/index.ts`) consumed via `transpilePackages`;
workers consume TS directly through tsx. Caveat learned in build: intra-package relative
imports must be extensionless (`./geo`, not `./geo.js`) or Turbopack resolves the module
to zero exports.
## ADR-003 — Separate admin app behind HTTP Basic Auth
The spec requires a "protected administration application." A distinct Next.js app on
:3101 keeps the review/ops surface out of the consumer bundle and puts one
`middleware.ts` Basic-Auth gate (BASIC_AUTH env) in front of everything. Shared UI
(badges, created-date chips) lives in `packages/shared-ui`.
## ADR-004 — pg-boss over Redis/BullMQ; stages are pure functions
Jobs live in Postgres (pg-boss) — no extra infra, and "what ran when" is queryable in
the same database as the pipeline audit trail. Every pipeline stage
(fetch/extract/validate/publish) is a plain async function; the queue only transports
invocations. The CLI (`pnpm pipeline`) and integration tests call stages directly, so
the queue is never required for correctness.
## ADR-005 — Evidence-first data model; deterministic validators gate publish
Every extracted field is a `FieldValue {value|null, raw, evidenceText, sourceUrl,
confidence}`. `value: null` means "source did not state it" — values are never inferred.
zod checks shape; the rule engine (`packages/validation`) checks truth-plausibility and
emits `ValidationEvent` rows; verdict `publishable | needs_review | rejected` is folded
deterministically (any failed error → rejected; any failed review → needs_review). The
LLM normalization hook exists but can never bless a record into publishing.
## ADR-006 — Organic-only ordering; objective filters (Fair Housing)
No paid-placement concept exists anywhere in the v1 schema or code, so organic order
cannot be contaminated. All search filters are objective attributes (price, beds,
status, HOA, school district name, age-restriction flag). No subjective neighborhood
dimensions exist in the schema at all — this is deliberate Fair Housing hygiene, not an
omission.
## ADR-007 — publisher package is the only writer of published tables
`@homesonspec/publisher.publishStagedRecord` is the single bridge from staged →
published, and it throws on any staged status other than VALIDATED (validators passed)
or APPROVED (human review). Collectors and extractors end at `StagedRecord`. The
integration suite asserts this invariant directly.
## ADR-008 — Verification label derives from collection method
`SourceRegistry.collectionMethod` → label: FEED → VERIFIED_FROM_BUILDER, CRAWL →
BUILDER_WEBSITE, MANUAL → BUILDER_SUBMITTED, FIXTURE/SYNTHETIC → DEMONSTRATION (+
`isDemo: true`). A source can never claim a stronger verification than how its data was
actually collected.
## ADR-009 — Leaflet + OSM behind a props-only MapView contract
$0, no keys. `MapView` accepts only {markers, center, zoom, onMoveEnd} so a commercial
provider can swap in without touching callers. MapView is deliberately excluded from the
shared-ui barrel (react-leaflet touches `window` at module scope) and imported by
subpath behind `next/dynamic ssr:false`.