Bhargav Urs Sumantharaj

MappedIn

not yet deployed

A travel portfolio on a rotatable 3D globe where the geography is computed in PostGIS rather than faked in JavaScript.

Build log: The repository carries a full engineering build log recording every problem hit during the build and how each one was solved.

Architecture

  • Next.js 16 + MapLibre GL (globe client), client. Renders the rotatable globe, draws journey arcs from server-computed geometry, and manages auth and place lists. Connected to Security filter chain over REST + JWT. Connected to OpenStreetMap Nominatim over geocoding. Connected to OpenFreeMap over vector tiles.
  • Security filter chain (hand-written), service. Orders every security concern explicitly: CORS, exception translation, the JWT filter, and per-route authorisation rules. Connected from Next.js 16 + MapLibre GL over REST + JWT. Connected to JWT filter.
  • JWT filter (HS256, Nimbus JOSE), service. Validates HS256 tokens via Nimbus JOSE and installs the authenticated principal for the request. Connected from Security filter chain. Connected to Ownership-scoped repositories.
  • Ownership-scoped repositories, service. Every query is scoped by owner at the repository signature: no method can fetch a list without being told whose it is. Connected from JWT filter. Connected to PostgreSQL 18 over JDBC.
  • Flyway (7 migrations), service. Applies the seven migrations at boot, so schema state is a property of the code rather than of whoever touched the database last. Connected to PostgreSQL 18 over DDL. Connected from Testcontainers over integration tests.
  • PostgreSQL 18 (PostGIS 3.6, uuidv7()), datastore. Stores users, places, lists, and memberships; generates UUIDv7 keys natively; runs all geography in SQL. Connected from Ownership-scoped repositories over JDBC. Connected from Flyway over DDL. Connected to ST_DWithin.
  • ST_DWithin (GIST index scan), datastore. Answers proximity queries as an index scan over the GIST index. Connected from PostgreSQL 18. Connected to ST_Segmentize.
  • ST_Segmentize (geography(Point,4326)), datastore. Densifies each journey line along the true geodesic so arcs drape over the sphere instead of cutting through it. Connected from ST_DWithin. Connected to ST_Distance.
  • ST_Distance (spheroid metres), datastore. Reports real spheroid metres between places, used for journey totals. Connected from ST_Segmentize.
  • OpenStreetMap Nominatim (geocoding), external. Turns a typed place name into candidate coordinates, ranked by Wikipedia-derived prominence. Connected from Next.js 16 + MapLibre GL over geocoding.
  • OpenFreeMap (vector tiles), external. Serves the vector tiles MapLibre renders. No key, no meter. Connected from Next.js 16 + MapLibre GL over vector tiles.
  • Testcontainers (real PostGIS), service. Runs the backend suite against a real PostGIS container, never H2. Connected to Flyway over integration tests.
  • Boundary: Spring Boot 4.1, Java 25.
  • Boundary: PostgreSQL 18 + PostGIS 3.6.
  • Boundary: Test boundary.

Decisions

  • Geography lives in the database

    The decision
    Proximity is ST_DWithin against a GIST index; journey arcs come from ST_Segmentize on a geography column, which densifies each line along the true geodesic; distances are real spheroid metres from ST_Distance.
    The constraint that forced it
    Routes must drape over the sphere and distances must be true metres, not planar approximations.
    Rejected
    Faking the geography in JavaScript with client-side interpolation.
    Consequence accepted
    The database is the geometry engine. Query cost and tuning live in SQL, and the client stays thin.
  • Index behaviour verified, not assumed

    The decision
    EXPLAIN (ANALYZE, BUFFERS) confirms ST_DWithin produces an index scan.
    The constraint that forced it
    The equivalent ST_Distance(...) < n degrades to a sequential scan because the planner cannot convert a scalar comparison into a box predicate.
    Rejected
    Trusting that any spatial predicate would use the spatial index.
    Consequence accepted
    Query shapes are pinned by evidence. A regression shows up as a plan change, not as a slow page.
  • Authorization is structural, not procedural

    The decision
    No repository method can fetch a list without being told whose it is, so "forgot the permission check" is not a mistake the code permits.
    The constraint that forced it
    Ownership rules must hold across every current and future endpoint.
    Rejected
    Procedural permission checks sprinkled ahead of generic repository calls.
    Consequence accepted
    Another user's resource returns 404 rather than 403, so record identifiers cannot be enumerated by diffing status codes.
  • UUIDv7 primary keys, generated in the database

    The decision
    Primary keys are UUIDv7, generated natively by PostgreSQL 18.
    The constraint that forced it
    Random UUIDv4 fragments B-tree pages on insert; sequential identifiers invite enumeration.
    Rejected
    A bigint would be smaller and naturally ordered, but the 404-not-403 defence is far weaker against sequential identifiers.
    Consequence accepted
    Time-ordered inserts append at the right edge of the B-tree, and keys stay opaque to users.
  • List membership is an entity

    The decision
    List membership is a first-class association entity carrying visited_on and notes.
    The constraint that forced it
    The relationship itself holds data: when a place was visited, and what was noted there.
    Rejected
    A JPA @ManyToMany, which has nowhere to put relationship attributes and rewrites every join row on modification.
    Consequence accepted
    One more entity and repository to maintain, in exchange for stable rows and a home for the data.
  • Tests run against real PostGIS

    The decision
    The backend suite runs against real PostGIS through Testcontainers, never H2.
    The constraint that forced it
    uuidv7(), geography, GIST, ST_DWithin, and the trigger functions have no H2 equivalent, so H2 tests would pass while proving nothing.
    Rejected
    H2 in a compatibility mode.
    Consequence accepted
    Docker becomes a test dependency, and container start time is paid on every run.
  • The geocoder was chosen by measurement

    The decision
    Geocoding ranks candidates by Wikipedia-derived prominence.
    The constraint that forced it
    The first candidate ranked "Eiffel Tower" as a hill in Alberta and "Mount Fuji" as a location in Wisconsin.
    Rejected
    Keeping the first geocoder for its friendlier rate limit.
    Consequence accepted
    A stricter rate limit that the search UI was then designed around.
  • The antimeridian is handled, and pinned by tests

    The decision
    Longitude unwrapping keeps journey arcs continuous across the antimeridian.
    The constraint that forced it
    A naive renderer draws a Pacific crossing as a line around the whole planet.
    Rejected
    Clamping longitudes to [-180, 180] and accepting the wrap.
    Consequence accepted
    Tests assert both that no step exceeds 180 degrees and that vertices are allowed to fall outside the [-180, 180] range.

Known limitations

  • The server-side bounding box is antimeridian-naive, and a test asserts that wrong behaviour on purpose, so fixing it flips a test rather than going unnoticed.
  • There is no Content-Security-Policy, which is the main missing compensating control given that the token is held in localStorage.
  • There is no token revocation path.