diff --git a/.github/workflows/nearby-places.yml b/.github/workflows/nearby-places.yml
new file mode 100644
index 00000000..bcfb148e
--- /dev/null
+++ b/.github/workflows/nearby-places.yml
@@ -0,0 +1,35 @@
+name: Nearby Places checks
+
+on:
+ pull_request:
+ paths:
+ - 'applications/nearby-places/**'
+ - '.github/workflows/nearby-places.yml'
+ push:
+ branches: [main]
+ paths:
+ - 'applications/nearby-places/**'
+ - '.github/workflows/nearby-places.yml'
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ check:
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: applications/nearby-places
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: denoland/setup-deno@22d081ff2d3a40755e97629de92e3bcbfa7cf2ed # v2 branch
+ with:
+ deno-version: 2.9.7
+ - run: deno install --frozen --entrypoint src/server.ts test/domain_test.ts test/cloud_test.ts
+ - run: deno task fmt:check
+ - run: deno task lint
+ - run: deno task check
+ - run: deno task test
+
+# Live PostGIS/browser checks require an explicitly provisioned dedicated fixture.
diff --git a/applications/nearby-places/.deno-version b/applications/nearby-places/.deno-version
new file mode 100644
index 00000000..10201185
--- /dev/null
+++ b/applications/nearby-places/.deno-version
@@ -0,0 +1 @@
+2.9.7
diff --git a/applications/nearby-places/.env.example b/applications/nearby-places/.env.example
new file mode 100644
index 00000000..745241a5
--- /dev/null
+++ b/applications/nearby-places/.env.example
@@ -0,0 +1,8 @@
+PGHOST=your-service-hostname
+PGPORT=5432
+PGDATABASE=postgres
+PGUSER=places_reader
+PGPASSWORD=replace-with-read-only-runtime-password
+PGSSLROOTCERT=/absolute/path/cloud-ca.pem
+PGSSLMODE=verify-full
+PORT=4000
diff --git a/applications/nearby-places/.gitignore b/applications/nearby-places/.gitignore
new file mode 100644
index 00000000..f9e87ddd
--- /dev/null
+++ b/applications/nearby-places/.gitignore
@@ -0,0 +1,4 @@
+.env
+*.pem
+node_modules/
+.deno/
diff --git a/applications/nearby-places/README.md b/applications/nearby-places/README.md
new file mode 100644
index 00000000..47f624c4
--- /dev/null
+++ b/applications/nearby-places/README.md
@@ -0,0 +1,246 @@
+# Nearby places
+
+A small Deno/Oak browser form and JSON API search a synthetic place directory in ClickHouse Managed
+Postgres. PostGIS stores `geography(Point,4326)` and returns spheroidal geodesic
+distances in meters. The results describe proximity over the Earth's surface, not routes or travel
+time.
+
+The app has no accounts or mutation routes. Its database role can select the directory, while
+separate administrator and schema-owner commands install PostGIS, migrate and seed. All labels are
+fictional; coordinates are explicit test fixtures. It does not request geolocation, use map tiles,
+geocode addresses or collect a real location directory.
+
+## Pinned stack
+
+- Deno 2.9.7, Oak 17.2.0 and npm Postgres.js 3.4.9.
+- Committed `deno.lock` pins transitive JSR/npm dependencies. There is no npm install or
+ node_modules requirement for the application.
+- Actual Cloud fixture on 2 October 2026: PostgreSQL 18.6, PostGIS 3.6.4. Setup prints
+ `pg_extension` and `PostGIS_Full_Version()`; inspect your service's actual version rather than
+ assuming this one.
+
+The query lives in [src/places.ts](src/places.ts), validation in [src/input.ts](src/input.ts), and
+connection verification in [src/database.ts](src/database.ts). Oak's standard `handle()` API runs
+its middleware on Deno's HTTP server; shutdown awaits `server.finished`, removes signal listeners
+and closes the database pool.
+
+## Create a dedicated Cloud fixture
+
+Use an authenticated clickhousectl and a dedicated test service. The following modest shape was
+verified in `us-east-1`; check current availability/pricing before allocating your own. This example
+has no HA.
+
+```bash
+export ORG_ID=your-organization-id
+clickhousectl cloud postgres create --org-id "$ORG_ID" \
+ --name nearby-places --provider aws --region us-east-1 \
+ --size c6gd.large --pg-version 18 --ha-type none --json \
+ > /private/path/service.json
+chmod 600 /private/path/service.json
+export SERVICE_ID=your-returned-service-id
+```
+
+Save the returned hostname, port, username and password privately. Repeat this command until its
+state is `running`:
+
+```bash
+clickhousectl cloud postgres get "$SERVICE_ID" --org-id "$ORG_ID" --json
+clickhousectl cloud postgres certs get "$SERVICE_ID" --org-id "$ORG_ID" \
+ --output /private/path/cloud-ca.pem
+```
+
+See
+[ClickHouse Managed Postgres extensions](https://clickhouse.com/docs/products/managed-postgres/extensions).
+Running services incur charges. Keep receipts, credentials and CA files out of Git.
+
+## Install, migrate and seed
+
+Install the pinned Deno release and a PostgreSQL client. From this application directory, populate
+the dependency cache from the committed lock:
+
+```bash
+deno install --frozen --entrypoint src/server.ts test/domain_test.ts test/cloud_test.ts
+deno task fmt:check
+deno task lint
+deno task check
+deno task test
+```
+
+Create a private setup environment file. Use the administrator credentials from the receipt and two
+distinct random role passwords of at least 24 characters:
+
+```dotenv
+PGHOST=your-service-hostname
+PGPORT=5432
+PGDATABASE=postgres
+PGSSLROOTCERT=/absolute/path/cloud-ca.pem
+PGSSLMODE=verify-full
+ADMIN_USER=receipt-username
+ADMIN_PASSWORD=receipt-password
+MIGRATION_PASSWORD=distinct-random-schema-owner-password
+APP_PASSWORD=distinct-random-read-only-password
+```
+
+Export fields to child processes and bootstrap as the administrator:
+
+```bash
+set -a
+source /private/path/setup.env
+set +a
+export PGUSER="$ADMIN_USER" PGPASSWORD="$ADMIN_PASSWORD"
+psql -X -f sql/bootstrap.sql
+export PGUSER=places_migration PGPASSWORD="$MIGRATION_PASSWORD"
+psql -X -f sql/migrate.sql
+psql -X -f sql/seed.sql
+```
+
+`bootstrap.sql` installs the available PostGIS extension in `public`, creates both roles and the
+owned application schema, and prints the actual extension version. It runs once. The dedicated
+database's public schema CREATE and database CREATE/TEMP privileges are revoked from PUBLIC. The
+schema owner needs no database CREATE grant because the administrator creates its schema explicitly.
+
+`migrate.sql` records version 1, creates the places table and GiST geography/category indexes, and
+grants SELECT on that table to the reader. `seed.sql` upserts the 12 explicit fixtures and analyzes
+the table. Repeat migration and seed to verify reproduction. Administrator cleanup offers a
+destructive fixture reset; no production downgrade path is included. Runtime startup never performs
+DDL or seed writes.
+
+## Start with restricted permissions
+
+Create a private runtime file from [.env.example](.env.example), containing only the reader's
+credentials, connection fields and optional `PORT=4000`. Do not include administrator or migration
+passwords.
+
+```bash
+set -a; source /private/path/runtime.env; set +a
+unset ADMIN_USER ADMIN_PASSWORD MIGRATION_PASSWORD APP_PASSWORD
+bash scripts/run.sh
+```
+
+Open `http://127.0.0.1:4000`. The browser offers London, equator and dateline fixtures,
+category/limit controls and sorted result cards. A zero radius shows only coincident points. Display
+distances are rounded; ordering uses the database's full returned distances.
+
+The [run script](scripts/run.sh) uses `--frozen --cached-only` and grants:
+
+- Network access only to `127.0.0.1:$PORT` and the configured Cloud hostname/port.
+- File reading only for `public` assets and the configured CA file.
+- Environment reading only for `PG*`, `PORT`, `NODE_DEBUG` and `NODE_EXTRA_CA_CERTS`. The PG prefix
+ is needed because Postgres.js reads its configuration defaults; the two Node variables support the
+ verified compatibility path.
+
+It grants no write, subprocess, FFI or blanket `-A` permission. Postgres.js supplies the downloaded
+CA, `rejectUnauthorized:true` and the actual hostname to Node-compatible TLS.
+`PGSSLMODE=verify-full` configures `psql`; the application sets verification explicitly instead of
+relying on that variable.
+
+The reader's default read-only transaction setting is a client-changeable convenience. Table grants
+enforce persistent read-only access even if that default is disabled; schema/database grants prevent
+persistent and temporary DDL. The Cloud test disables the default before asserting SQLSTATE 42501
+for writes and DDL. The role can use public PostGIS functions needed by SELECT queries, and
+PostgreSQL's ordinary system catalog access.
+
+This is a public read-only sample bound to loopback, with a three-connection pool and two-second
+statement timeout. Public deployment would need HTTPS, traffic limits and appropriate operational
+monitoring. Searches aren't written to an application table or logged by this example;
+infrastructure/access logs are outside that claim.
+
+## JSON search
+
+```bash
+curl -sS 'http://127.0.0.1:4000/api/nearby?longitude=0&latitude=0&radiusMeters=1200&limit=20'
+curl -sS 'http://127.0.0.1:4000/api/nearby?longitude=179.999&latitude=0&radiusMeters=300&category=park'
+```
+
+`GET /api/nearby` returns `{ search, places, sampleData: true }`. Each place includes ID, label,
+category, longitude, latitude and `distanceMeters`. The search echoes validated inputs. Results are
+bounded by the supplied limit; there is no pagination or total-match count. Unsupported mutations
+return `405`.
+
+| Parameter | Bounds |
+| -------------- | -------------------------------------------------- |
+| `longitude` | Required finite decimal, −180 through 180 |
+| `latitude` | Required finite decimal, −90 through 90 |
+| `radiusMeters` | Required finite decimal, 0 through 50,000 |
+| `category` | Optional `cafe`, `library` or `park`; omit for all |
+| `limit` | Optional integer 1–50; defaults to 20 |
+
+Unknown/repeated parameters, nonfinite numbers, malformed decimals and values outside bounds return
+`400`. URLs longer than 2,048 characters return `414`. A database failure returns a generic `503`
+without connection details.
+
+The query constructs its origin in **longitude, latitude** order. Parameterized
+`ST_DWithin(geography, geography, meters, true)` restricts candidates and can use the GiST index.
+`ST_Distance(..., true)` supplies spheroidal meters for ordering, followed by place ID as the
+deterministic tie-breaker, then the bounded limit. It does not use a degree-based radius or
+spherical KNN distance as a substitute for the requested ordering.
+
+## Native Cloud and browser acceptance
+
+Use a freshly bootstrapped dedicated fixture. After exporting setup.env, switch to the reader:
+
+```bash
+export PGUSER=places_reader PGPASSWORD="$APP_PASSWORD"
+export EVIDENCE_DIR=/private/path/evidence
+mkdir -p "$EVIDENCE_DIR"
+deno test --frozen --cached-only \
+ --allow-env='PG*,EVIDENCE_DIR,NODE_DEBUG,NODE_EXTRA_CA_CERTS' \
+ --allow-net="$PGHOST:$PGPORT" --allow-read="$PGSSLROOTCERT,/tmp" \
+ --allow-write="/tmp,$EVIDENCE_DIR" --allow-run=openssl test/cloud_test.ts
+```
+
+The test needs OpenSSL only to generate an unrelated CA and writes evidence only to the configured
+location. It checks known equatorial meters, identical points, radius/category filters, coordinate
+order, stable ties/limits, antimeridian and polar behavior, grant denials after disabling the
+read-only default, and negative TLS controls with a positive connection afterward. It saves an
+actual `EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON)` for the application query. No index is forced; a
+tiny fixture's plan is evidence of that query shape, not a performance benchmark.
+
+The optional browser/HTTP harness uses Node 24.21.0 and Playwright 1.63.0 in an isolated development
+directory. It starts the same restricted application command, checks desktop/mobile cards and
+malformed HTTP requests, confirms the original process exits, then restarts it and compares durable
+results:
+
+```bash
+export BROWSER_DIR=/private/path/browser-dependencies
+mkdir -p "$BROWSER_DIR"
+npm --prefix "$BROWSER_DIR" install --save-exact playwright@1.63.0
+"$BROWSER_DIR/node_modules/.bin/playwright" install --with-deps chromium
+cp test/browser.mjs "$BROWSER_DIR/browser.mjs"
+export APP_DIR="$PWD" EVIDENCE_DIR=/private/path/evidence
+# Use runtime.env, with only reader credentials, for this harness.
+node "$BROWSER_DIR/browser.mjs"
+```
+
+CI runs frozen installation, formatting, lint, type checking and the three domain tests without
+Cloud secrets. Cloud/browser evidence belongs to the dedicated development fixture.
+
+## Cleanup
+
+Stop the application before resetting schemas or deleting the service. To reset only this fixture,
+explicitly restore administrator credentials:
+
+```bash
+set -a; source /private/path/setup.env; set +a
+export PGUSER="$ADMIN_USER" PGPASSWORD="$ADMIN_PASSWORD"
+psql -X -f sql/cleanup.sql
+```
+
+The reset keeps the administrator-owned PostGIS extension so bootstrap can be repeated.
+Database/public-schema privilege revocations also remain. To remove the billable fixture, use its
+exact recorded ID and verify absence after asynchronous deletion:
+
+```bash
+clickhousectl cloud postgres delete "$SERVICE_ID" --org-id "$ORG_ID"
+clickhousectl cloud postgres list --org-id "$ORG_ID" --json
+```
+
+Deletion destroys that service's data. Preserve needed source and evidence first.
+
+## Primary references
+
+- [PostGIS ST_DWithin](https://postgis.net/docs/ST_DWithin.html)
+- [PostGIS ST_Distance](https://postgis.net/docs/ST_Distance.html)
+- [Deno's npm Postgres example](https://docs.deno.com/examples/postgres/)
+- [Oak 17.2.0 source](https://jsr.io/@oak/oak@17.2.0)
+- [Postgres.js 3.4.9 source](https://github.com/porsager/postgres/tree/v3.4.9)
diff --git a/applications/nearby-places/deno.json b/applications/nearby-places/deno.json
new file mode 100644
index 00000000..aede0720
--- /dev/null
+++ b/applications/nearby-places/deno.json
@@ -0,0 +1,16 @@
+{
+ "imports": {
+ "@oak/oak": "jsr:@oak/oak@17.2.0",
+ "postgres": "npm:postgres@3.4.9"
+ },
+ "tasks": {
+ "check": "deno check --frozen src/server.ts test/domain_test.ts test/cloud_test.ts",
+ "test": "deno test --frozen test/domain_test.ts",
+ "fmt:check": "deno fmt --check",
+ "lint": "deno lint src test"
+ },
+ "fmt": {
+ "lineWidth": 100
+ },
+ "nodeModulesDir": "none"
+}
diff --git a/applications/nearby-places/deno.lock b/applications/nearby-places/deno.lock
new file mode 100644
index 00000000..43acb452
--- /dev/null
+++ b/applications/nearby-places/deno.lock
@@ -0,0 +1,87 @@
+{
+ "version": "5",
+ "specifiers": {
+ "jsr:@oak/commons@1": "1.0.1",
+ "jsr:@oak/oak@17.2.0": "17.2.0",
+ "jsr:@std/assert@1": "1.0.19",
+ "jsr:@std/bytes@1": "1.0.6",
+ "jsr:@std/crypto@1": "1.1.0",
+ "jsr:@std/encoding@1": "1.0.11",
+ "jsr:@std/encoding@^1.0.11": "1.0.11",
+ "jsr:@std/http@1": "1.1.4",
+ "jsr:@std/internal@^1.0.14": "1.0.14",
+ "jsr:@std/media-types@1": "1.1.0",
+ "jsr:@std/path@1": "1.1.6",
+ "npm:path-to-regexp@^6.3.0": "6.3.0",
+ "npm:postgres@3.4.9": "3.4.9"
+ },
+ "jsr": {
+ "@oak/commons@1.0.1": {
+ "integrity": "889ff210f0b4292591721be07244ecb1b5c118742f5273c70cf30d7cd4184d0c",
+ "dependencies": [
+ "jsr:@std/assert",
+ "jsr:@std/bytes",
+ "jsr:@std/crypto",
+ "jsr:@std/encoding@1",
+ "jsr:@std/http",
+ "jsr:@std/media-types"
+ ]
+ },
+ "@oak/oak@17.2.0": {
+ "integrity": "938537a92fc7922a46a9984696c65fb189c9baad164416ac3e336768a9ff0cd1",
+ "dependencies": [
+ "jsr:@oak/commons",
+ "jsr:@std/assert",
+ "jsr:@std/bytes",
+ "jsr:@std/http",
+ "jsr:@std/media-types",
+ "jsr:@std/path",
+ "npm:path-to-regexp"
+ ]
+ },
+ "@std/assert@1.0.19": {
+ "integrity": "eaada96ee120cb980bc47e040f82814d786fe8162ecc53c91d8df60b8755991e"
+ },
+ "@std/bytes@1.0.6": {
+ "integrity": "f6ac6adbd8ccd99314045f5703e23af0a68d7f7e58364b47d2c7f408aeb5820a"
+ },
+ "@std/crypto@1.1.0": {
+ "integrity": "b8d6d0a6377a32b213af2661ed7bf1062d94feac0c57def5526a8e74a95c3ec8"
+ },
+ "@std/encoding@1.0.11": {
+ "integrity": "e7cef2f0b3153bccc17431e7c864a1de03a4b6d9647389c43e08aaa775d37385"
+ },
+ "@std/http@1.1.4": {
+ "integrity": "4f1575226ca7c0df1df38b26d50265fed30869b2a63ab867edc5b92492150c8c",
+ "dependencies": [
+ "jsr:@std/encoding@^1.0.11"
+ ]
+ },
+ "@std/internal@1.0.14": {
+ "integrity": "291516b3d4c35024d6ffbc0a9df5bf4c64116e05b50012cf846710152d2ffdf7"
+ },
+ "@std/media-types@1.1.0": {
+ "integrity": "c9d093f0c05c3512932b330e3cc1fe1d627b301db33a4c2c2185c02471d6eaa4"
+ },
+ "@std/path@1.1.6": {
+ "integrity": "c68485c2a4dfbb5ae3cc74fae4e8c4e5d874cf8a8ed12927917235c758b46cbe",
+ "dependencies": [
+ "jsr:@std/internal"
+ ]
+ }
+ },
+ "npm": {
+ "path-to-regexp@6.3.0": {
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="
+ },
+ "postgres@3.4.9": {
+ "integrity": "sha512-GD3qdB0x1z9xgFI6cdRD6xu2Sp2WCOEoe3mtnyB5Ee0XrrL5Pe+e4CCnJrRMnL1zYtRDZmQQVbvOttLnKDLnaw=="
+ }
+ },
+ "workspace": {
+ "dependencies": [
+ "jsr:@oak/oak@17.2.0",
+ "npm:postgres@3.4.9"
+ ]
+ }
+}
diff --git a/applications/nearby-places/public/app.js b/applications/nearby-places/public/app.js
new file mode 100644
index 00000000..f1ad99af
--- /dev/null
+++ b/applications/nearby-places/public/app.js
@@ -0,0 +1,86 @@
+const form = document.querySelector("#search-form");
+const results = document.querySelector("#results");
+const status = document.querySelector("#status");
+const submit = document.querySelector("#submit");
+let controller;
+let sequence = 0;
+const presets = {
+ london: { longitude: -0.12, latitude: 51.5, radiusMeters: 1000 },
+ equator: { longitude: 0, latitude: 0, radiusMeters: 1200 },
+ dateline: { longitude: 179.999, latitude: 0, radiusMeters: 300 },
+};
+for (const button of document.querySelectorAll("[data-preset]")) {
+ button.addEventListener("click", () => {
+ for (const [name, value] of Object.entries(presets[button.dataset.preset])) {
+ form.elements.namedItem(name).value = String(value);
+ }
+ form.elements.namedItem("category").value = "";
+ form.requestSubmit();
+ });
+}
+form.addEventListener("submit", (event) => {
+ event.preventDefault();
+ void search();
+});
+async function search() {
+ const current = ++sequence;
+ controller?.abort();
+ controller = new AbortController();
+ const params = new URLSearchParams();
+ for (const [key, value] of new FormData(form)) {
+ if (value !== "") params.set(key, String(value));
+ }
+ results.replaceChildren();
+ status.textContent = "Looking around your point…";
+ submit.disabled = true;
+ try {
+ const response = await fetch(`/api/nearby?${params}`, { signal: controller.signal });
+ const data = await response.json();
+ if (!response.ok) throw new Error(data.error ?? "Search unavailable");
+ if (current !== sequence) return;
+ status.textContent = data.places.length
+ ? `${data.places.length} sample ${
+ data.places.length === 1 ? "place" : "places"
+ } within ${data.search.radiusMeters.toLocaleString()} meters`
+ : "No sample places within this radius. Try a larger radius or another sample.";
+ for (const place of data.places) {
+ const item = document.createElement("li");
+ const card = document.createElement("article");
+ card.className = "place-card";
+ const icon = document.createElement("span");
+ icon.className = `place-icon ${place.category}`;
+ icon.setAttribute("aria-hidden", "true");
+ icon.textContent = { cafe: "☕", library: "▤", park: "♧" }[place.category];
+ const details = document.createElement("div");
+ const category = document.createElement("p");
+ category.className = "place-category";
+ category.textContent = place.category;
+ const name = document.createElement("h3");
+ name.textContent = place.name;
+ const coordinates = document.createElement("p");
+ coordinates.className = "coordinates";
+ coordinates.textContent = `${place.longitude.toFixed(3)}° longitude · ${
+ place.latitude.toFixed(3)
+ }° latitude`;
+ details.append(category, name, coordinates);
+ const distance = document.createElement("div");
+ distance.className = "distance";
+ const value = document.createElement("strong");
+ value.textContent = place.distanceMeters < 1000
+ ? `${place.distanceMeters.toFixed(1)} m`
+ : `${(place.distanceMeters / 1000).toFixed(3)} km`;
+ const caption = document.createElement("span");
+ caption.textContent = "from your point";
+ distance.append(value, caption);
+ card.append(icon, details, distance);
+ item.append(card);
+ results.append(item);
+ }
+ } catch (error) {
+ if (current !== sequence || error.name === "AbortError") return;
+ status.textContent = error.message;
+ } finally {
+ if (current === sequence) submit.disabled = false;
+ }
+}
+void search();
diff --git a/applications/nearby-places/public/index.html b/applications/nearby-places/public/index.html
new file mode 100644
index 00000000..9560252d
--- /dev/null
+++ b/applications/nearby-places/public/index.html
@@ -0,0 +1,47 @@
+
+
+
+
+
+ Nearby places · A synthetic directory
+
+
+
+
+
+
+
+ A point. A radius. A few discoveries.
+ Find a little closer.
+ Explore cafes, libraries and parks around a point.
Every place in this directory is a made-up sample.
+
+
+
+ Choose your starting point
+ Enter coordinates or try a sample.
+
+
+
+
+
+ Around your point
Nearest first
+ Loading the London sample…
+
+
+
+
+
+
+
diff --git a/applications/nearby-places/public/style.css b/applications/nearby-places/public/style.css
new file mode 100644
index 00000000..c3385f8e
--- /dev/null
+++ b/applications/nearby-places/public/style.css
@@ -0,0 +1,350 @@
+:root {
+ font-family: Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;
+ color: #263a32;
+ background: #f6f7f2;
+ font-synthesis: none;
+}
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+main {
+ max-width: 1180px;
+ margin: 0 auto;
+ padding: 30px 40px;
+}
+a {
+ color: inherit;
+ text-decoration: none;
+}
+.masthead {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-bottom: 28px;
+ border-bottom: 1px solid #dce2d7;
+}
+.brand {
+ display: flex;
+ gap: 11px;
+ align-items: center;
+ font-size: 18px;
+ font-weight: 700;
+ letter-spacing: -.4px;
+}
+.brand-icon {
+ display: grid;
+ place-items: center;
+ width: 35px;
+ height: 35px;
+ background: #244d3b;
+ border-radius: 10px;
+ color: #e4eebe;
+ font-size: 25px;
+}
+.sample-badge {
+ border: 1px solid #ccd6c6;
+ border-radius: 30px;
+ font-size: 12px;
+ padding: 8px 13px;
+ color: #53624e;
+}
+.intro {
+ padding: 45px 0 34px;
+}
+.eyebrow {
+ color: #648063;
+ text-transform: uppercase;
+ letter-spacing: 1.8px;
+ font-size: 11px;
+ font-weight: 700;
+}
+h1 {
+ font-family: Georgia,serif;
+ font-size: 52px;
+ letter-spacing: -1.8px;
+ font-weight: 400;
+ margin: 12px 0 15px;
+ line-height: 1.06;
+}
+.intro > p:last-child {
+ color: #657269;
+ font-size: 15px;
+ line-height: 1.7;
+}
+.workspace {
+ display: grid;
+ grid-template-columns: 345px 1fr;
+ gap: 34px;
+ align-items: start;
+}
+.search-panel {
+ background: white;
+ border: 1px solid #dce2d7;
+ border-radius: 15px;
+ padding: 25px;
+}
+.search-panel h2 {
+ font-size: 17px;
+ letter-spacing: -.4px;
+ margin: 0 0 7px;
+}
+.muted {
+ font-size: 12px;
+ color: #788177;
+ margin: 0 0 18px;
+}
+.presets {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin-bottom: 25px;
+}
+button,
+input,
+select {
+ font: inherit;
+}
+button {
+ cursor: pointer;
+}
+.presets button {
+ border: 1px solid #dde4d7;
+ border-radius: 6px;
+ background: #f7f9f2;
+ color: #486148;
+ padding: 7px 8px;
+ font-size: 10px;
+}
+.presets button:hover {
+ background: #e9efd9;
+}
+label {
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ margin-bottom: 17px;
+}
+input,
+select {
+ display: block;
+ width: 100%;
+ padding: 11px 12px;
+ margin-top: 7px;
+ border: 1px solid #d4dbcf;
+ border-radius: 7px;
+ background: #fff;
+ color: #344538;
+ font-size: 13px;
+}
+.coordinate-fields {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 13px;
+}
+input:focus,
+select:focus {
+ outline: 2px solid #7e986c;
+ outline-offset: 2px;
+}
+.primary {
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+ padding: 13px 15px;
+ border: 0;
+ border-radius: 7px;
+ background: #2c533f;
+ color: white;
+ font-size: 13px;
+ font-weight: 600;
+}
+.primary:hover {
+ background: #1f432f;
+}
+.primary:disabled {
+ opacity: .6;
+ cursor: wait;
+}
+.distance-note {
+ display: flex;
+ gap: 11px;
+ border-top: 1px solid #e6e9df;
+ margin-top: 23px;
+ padding-top: 17px;
+ color: #7a8478;
+ font-size: 11px;
+ line-height: 1.6;
+}
+.distance-note > span {
+ font-size: 24px;
+ color: #6f856b;
+}
+.distance-note p {
+ margin: 0;
+}
+.results-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 2px 0;
+}
+.results-top h2 {
+ font-size: 21px;
+ letter-spacing: -.6px;
+ font-weight: 600;
+ margin: 0;
+}
+.sort-note {
+ font-size: 11px;
+ color: #7b8677;
+}
+#status {
+ color: #7b8677;
+ font-size: 12px;
+ line-height: 1.5;
+ margin: 10px 0 22px;
+}
+#results {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: 12px;
+}
+.place-card {
+ display: grid;
+ grid-template-columns: 48px 1fr auto;
+ gap: 16px;
+ align-items: center;
+ border: 1px solid #dce2d7;
+ background: #fff;
+ border-radius: 12px;
+ padding: 19px 20px;
+}
+.place-icon {
+ display: grid;
+ place-items: center;
+ width: 47px;
+ height: 47px;
+ border-radius: 12px;
+ font-size: 24px;
+ background: #f2eedf;
+ color: #967947;
+}
+.library {
+ background: #e9edf3;
+ color: #677a98;
+}
+.park {
+ background: #eaf0df;
+ color: #658150;
+}
+.place-category {
+ text-transform: uppercase;
+ font-size: 9px;
+ letter-spacing: 1.5px;
+ color: #8b9584;
+ margin: 0 0 5px;
+}
+h3 {
+ font-size: 17px;
+ font-weight: 600;
+ letter-spacing: -.4px;
+ margin: 0;
+}
+.coordinates {
+ font-size: 10px;
+ color: #8a9485;
+ margin: 7px 0 0;
+}
+.distance {
+ text-align: right;
+ padding-left: 8px;
+}
+.distance strong {
+ display: block;
+ font-size: 17px;
+ font-weight: 600;
+ color: #3a5942;
+}
+.distance span {
+ display: block;
+ font-size: 9px;
+ color: #8a9485;
+ margin-top: 5px;
+}
+footer {
+ border-top: 1px solid #dce2d7;
+ padding-top: 20px;
+ margin-top: 43px;
+ font-size: 11px;
+ color: #8a9485;
+ display: flex;
+ justify-content: space-between;
+}
+@media (max-width:800px) {
+ main {
+ padding: 20px;
+ }
+ .workspace {
+ grid-template-columns: 1fr;
+ gap: 26px;
+ }
+ .intro {
+ padding: 32px 0 20px;
+ }
+ h1 {
+ font-size: 42px;
+ }
+ .search-panel {
+ padding: 22px;
+ }
+ .place-card {
+ padding: 15px;
+ gap: 12px;
+ }
+ .coordinates {
+ font-size: 9px;
+ }
+ .distance strong {
+ font-size: 15px;
+ }
+ footer {
+ gap: 15px;
+ line-height: 1.6;
+ }
+}
+@media (max-width:430px) {
+ main {
+ padding: 15px;
+ }
+ .sample-badge {
+ font-size: 10px;
+ padding: 7px 9px;
+ }
+ .brand {
+ font-size: 16px;
+ }
+ .place-card {
+ grid-template-columns: 35px 1fr auto;
+ gap: 10px;
+ }
+ .place-icon {
+ width: 35px;
+ height: 35px;
+ font-size: 20px;
+ }
+ h3 {
+ font-size: 14px;
+ }
+ .coordinates {
+ max-width: 160px;
+ line-height: 1.5;
+ }
+ .distance span {
+ font-size: 8px;
+ }
+}
diff --git a/applications/nearby-places/scripts/run.sh b/applications/nearby-places/scripts/run.sh
new file mode 100755
index 00000000..6d92df69
--- /dev/null
+++ b/applications/nearby-places/scripts/run.sh
@@ -0,0 +1,10 @@
+#!/usr/bin/env bash
+set -euo pipefail
+: "${PGHOST:?}" "${PGPORT:=5432}" "${PGSSLROOTCERT:?}"
+: "${PORT:=4000}"
+# PG* is required by Postgres.js's configuration defaults. No write/run/FFI grants.
+exec deno run --frozen --cached-only \
+ --allow-net="127.0.0.1:$PORT,$PGHOST:$PGPORT" \
+ --allow-read="public,$PGSSLROOTCERT" \
+ --allow-env='PG*,PORT,NODE_DEBUG,NODE_EXTRA_CA_CERTS' \
+ src/server.ts
diff --git a/applications/nearby-places/sql/bootstrap.sql b/applications/nearby-places/sql/bootstrap.sql
new file mode 100644
index 00000000..a020e650
--- /dev/null
+++ b/applications/nearby-places/sql/bootstrap.sql
@@ -0,0 +1,24 @@
+\set ON_ERROR_STOP on
+\getenv migration_password MIGRATION_PASSWORD
+\getenv runtime_password APP_PASSWORD
+SELECT length(:'migration_password') >= 24 AND length(:'runtime_password') >= 24
+ AND :'migration_password' <> :'runtime_password' AS passwords_valid \gset
+\if :passwords_valid
+\else
+ DO $$ BEGIN RAISE EXCEPTION 'Use distinct random passwords of at least 24 characters'; END $$;
+\endif
+BEGIN;
+CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public;
+CREATE ROLE places_migration LOGIN PASSWORD :'migration_password';
+CREATE ROLE places_reader LOGIN PASSWORD :'runtime_password';
+REVOKE CREATE ON SCHEMA public FROM PUBLIC;
+SELECT format('REVOKE CREATE, TEMPORARY ON DATABASE %I FROM PUBLIC', current_database()) \gexec
+CREATE SCHEMA nearby_places AUTHORIZATION places_migration;
+REVOKE ALL ON SCHEMA nearby_places FROM PUBLIC;
+GRANT USAGE ON SCHEMA nearby_places TO places_reader;
+ALTER ROLE places_reader SET default_transaction_read_only = on;
+ALTER ROLE places_reader SET statement_timeout = '2s';
+ALTER ROLE places_reader SET idle_in_transaction_session_timeout = '5s';
+COMMIT;
+SELECT extname, extversion FROM pg_extension WHERE extname = 'postgis';
+SELECT public.PostGIS_Full_Version();
diff --git a/applications/nearby-places/sql/cleanup.sql b/applications/nearby-places/sql/cleanup.sql
new file mode 100644
index 00000000..336693a0
--- /dev/null
+++ b/applications/nearby-places/sql/cleanup.sql
@@ -0,0 +1,6 @@
+\set ON_ERROR_STOP on
+-- Administrator-only reset for this dedicated fixture, after the app stops.
+DROP SCHEMA IF EXISTS nearby_places CASCADE;
+DROP ROLE IF EXISTS places_reader;
+DROP ROLE IF EXISTS places_migration;
+-- Keep the administrator-owned PostGIS extension for bootstrap repetition.
diff --git a/applications/nearby-places/sql/migrate.sql b/applications/nearby-places/sql/migrate.sql
new file mode 100644
index 00000000..8c7bfa68
--- /dev/null
+++ b/applications/nearby-places/sql/migrate.sql
@@ -0,0 +1,23 @@
+\set ON_ERROR_STOP on
+BEGIN;
+CREATE TABLE IF NOT EXISTS nearby_places.schema_migrations (
+ version integer PRIMARY KEY,
+ applied_at timestamptz NOT NULL DEFAULT clock_timestamp()
+);
+SELECT NOT EXISTS (SELECT 1 FROM nearby_places.schema_migrations WHERE version = 1)
+ AS apply_migration \gset
+\if :apply_migration
+CREATE TABLE nearby_places.places (
+ id integer PRIMARY KEY,
+ name varchar(80) NOT NULL CHECK (length(btrim(name)) BETWEEN 1 AND 80),
+ category text NOT NULL CHECK (category IN ('cafe', 'library', 'park')),
+ location public.geography(Point, 4326) NOT NULL
+);
+CREATE INDEX places_location_gist ON nearby_places.places USING gist (location);
+CREATE INDEX places_category ON nearby_places.places (category);
+INSERT INTO nearby_places.schema_migrations (version) VALUES (1);
+\else
+\echo Migration already applied
+\endif
+GRANT SELECT ON nearby_places.places TO places_reader;
+COMMIT;
diff --git a/applications/nearby-places/sql/seed.sql b/applications/nearby-places/sql/seed.sql
new file mode 100644
index 00000000..6ada0861
--- /dev/null
+++ b/applications/nearby-places/sql/seed.sql
@@ -0,0 +1,22 @@
+\set ON_ERROR_STOP on
+-- Synthetic labels at explicit test coordinates, not a directory of real businesses.
+INSERT INTO nearby_places.places (id, name, category, location)
+SELECT id, name, category,
+ public.ST_SetSRID(public.ST_MakePoint(longitude, latitude), 4326)::public.geography
+FROM (VALUES
+ (1, 'Anchor Cafe', 'cafe', -0.120::double precision, 51.500::double precision),
+ (2, 'Garden Library', 'library', -0.119, 51.501),
+ (3, 'Quay Park', 'park', -0.122, 51.499),
+ (4, 'Courtyard Cafe', 'cafe', -0.120, 51.500),
+ (5, 'Further Cafe', 'cafe', -0.110, 51.500),
+ (6, 'Equator Library', 'library', 0.000, 0.000),
+ (7, 'Equator East Cafe', 'cafe', 0.010, 0.000),
+ (8, 'Equator North Park', 'park', 0.000, 0.010),
+ (9, 'Dateline East Cafe', 'cafe', 179.999, 0.000),
+ (10, 'Dateline West Park', 'park', -179.999, 0.000),
+ (11, 'Off-axis Library', 'library', 12.000, 45.000),
+ (12, 'Polar Garden', 'park', 0.000, 89.999)
+) AS fixture(id, name, category, longitude, latitude)
+ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, category = EXCLUDED.category,
+ location = EXCLUDED.location;
+ANALYZE nearby_places.places;
diff --git a/applications/nearby-places/src/app.ts b/applications/nearby-places/src/app.ts
new file mode 100644
index 00000000..62cf1837
--- /dev/null
+++ b/applications/nearby-places/src/app.ts
@@ -0,0 +1,54 @@
+import { Application, Router } from "@oak/oak";
+import { InputError, parseSearch } from "./input.ts";
+import { nearbyQuery } from "./places.ts";
+import type { Database } from "./database.ts";
+export function createApp(sql: Database, assets: Record) {
+ const app = new Application();
+ app.use(async (ctx, next) => {
+ ctx.response.headers.set(
+ "Content-Security-Policy",
+ "default-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'",
+ );
+ ctx.response.headers.set("X-Content-Type-Options", "nosniff");
+ ctx.response.headers.set("Referrer-Policy", "no-referrer");
+ try {
+ if (ctx.request.url.href.length > 2048) {
+ ctx.response.status = 414;
+ ctx.response.body = { error: "url_too_long" };
+ return;
+ }
+ await next();
+ } catch (error) {
+ if (error instanceof InputError) {
+ ctx.response.status = 400;
+ ctx.response.body = { error: error.message };
+ } else {
+ console.error("Nearby search failed");
+ ctx.response.status = 503;
+ ctx.response.body = { error: "search_unavailable" };
+ }
+ }
+ });
+ const router = new Router();
+ router.get("/health", (ctx) => {
+ ctx.response.body = { ok: true };
+ });
+ router.get("/api/nearby", async (ctx) => {
+ const search = parseSearch(ctx.request.url.searchParams);
+ ctx.response.body = { search, places: await nearbyQuery(sql, search), sampleData: true };
+ ctx.response.headers.set("Cache-Control", "no-store");
+ });
+ for (const [path, asset] of Object.entries(assets)) {
+ router.get(path, (ctx) => {
+ ctx.response.type = asset.type;
+ ctx.response.body = asset.body;
+ });
+ }
+ app.use(router.routes());
+ app.use(router.allowedMethods());
+ app.use((ctx) => {
+ ctx.response.status = 404;
+ ctx.response.body = { error: "not_found" };
+ });
+ return app;
+}
diff --git a/applications/nearby-places/src/database.ts b/applications/nearby-places/src/database.ts
new file mode 100644
index 00000000..9393d56d
--- /dev/null
+++ b/applications/nearby-places/src/database.ts
@@ -0,0 +1,32 @@
+import postgres from "postgres";
+export function databaseOptions() {
+ const required = (key: string): string => {
+ const value = Deno.env.get(key);
+ if (!value) throw new Error(`${key} is required`);
+ return value;
+ };
+ const host = required("PGHOST");
+ const port = Number(Deno.env.get("PGPORT") ?? "5432");
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid PGPORT");
+ return {
+ host,
+ port,
+ database: required("PGDATABASE"),
+ username: required("PGUSER"),
+ password: required("PGPASSWORD"),
+ ssl: {
+ ca: Deno.readTextFileSync(required("PGSSLROOTCERT")),
+ rejectUnauthorized: true,
+ servername: host,
+ },
+ max: 3,
+ connect_timeout: 15,
+ idle_timeout: 20,
+ fetch_types: false,
+ connection: { application_name: "nearby-places", statement_timeout: 2000 },
+ };
+}
+export function database() {
+ return postgres(databaseOptions());
+}
+export type Database = ReturnType;
diff --git a/applications/nearby-places/src/input.ts b/applications/nearby-places/src/input.ts
new file mode 100644
index 00000000..27d1f9d6
--- /dev/null
+++ b/applications/nearby-places/src/input.ts
@@ -0,0 +1,47 @@
+export const CATEGORIES = ["cafe", "library", "park"] as const;
+export type Category = typeof CATEGORIES[number];
+export type Search = {
+ longitude: number;
+ latitude: number;
+ radiusMeters: number;
+ limit: number;
+ category?: Category;
+};
+export class InputError extends Error {}
+const decimal = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
+export function parseSearch(params: URLSearchParams): Search {
+ const allowed = new Set(["longitude", "latitude", "radiusMeters", "limit", "category"]);
+ const seen = new Set();
+ for (const key of params.keys()) {
+ if (!allowed.has(key) || seen.has(key)) throw new InputError("Unknown or repeated parameter");
+ seen.add(key);
+ }
+ const number = (key: string, minimum: number, maximum: number): number => {
+ const raw = params.get(key);
+ if (raw === null || !decimal.test(raw)) throw new InputError(`Invalid ${key}`);
+ const value = Number(raw);
+ if (!Number.isFinite(value) || value < minimum || value > maximum) {
+ throw new InputError(`${key} must be between ${minimum} and ${maximum}`);
+ }
+ return value;
+ };
+ const longitude = number("longitude", -180, 180);
+ const latitude = number("latitude", -90, 90);
+ const radiusMeters = number("radiusMeters", 0, 50_000);
+ const rawLimit = params.get("limit") ?? "20";
+ const limit = Number(rawLimit);
+ if (!/^\d+$/.test(rawLimit) || !Number.isInteger(limit) || limit < 1 || limit > 50) {
+ throw new InputError("limit must be an integer between 1 and 50");
+ }
+ const category = params.get("category");
+ if (category !== null && !CATEGORIES.includes(category as Category)) {
+ throw new InputError("category must be cafe, library or park");
+ }
+ return {
+ longitude,
+ latitude,
+ radiusMeters,
+ limit,
+ ...(category ? { category: category as Category } : {}),
+ };
+}
diff --git a/applications/nearby-places/src/places.ts b/applications/nearby-places/src/places.ts
new file mode 100644
index 00000000..6e85dd5e
--- /dev/null
+++ b/applications/nearby-places/src/places.ts
@@ -0,0 +1,29 @@
+import type { Database } from "./database.ts";
+import type { Search } from "./input.ts";
+export type Place = {
+ id: number;
+ name: string;
+ category: string;
+ longitude: number;
+ latitude: number;
+ distanceMeters: number;
+};
+export function nearbyQuery(sql: Database, search: Search) {
+ return sql`
+ WITH origin AS (
+ SELECT public.ST_SetSRID(
+ public.ST_MakePoint(${search.longitude}::double precision, ${search.latitude}::double precision),
+ 4326
+ )::public.geography AS location
+ )
+ SELECT p.id, p.name, p.category,
+ public.ST_X(p.location::public.geometry) AS longitude,
+ public.ST_Y(p.location::public.geometry) AS latitude,
+ public.ST_Distance(p.location, o.location, true) AS "distanceMeters"
+ FROM nearby_places.places p CROSS JOIN origin o
+ WHERE public.ST_DWithin(p.location, o.location, ${search.radiusMeters}::double precision, true)
+ ${search.category ? sql`AND p.category = ${search.category}` : sql``}
+ ORDER BY "distanceMeters", p.id
+ LIMIT ${search.limit}
+ `;
+}
diff --git a/applications/nearby-places/src/server.ts b/applications/nearby-places/src/server.ts
new file mode 100644
index 00000000..3ba2ebb7
--- /dev/null
+++ b/applications/nearby-places/src/server.ts
@@ -0,0 +1,35 @@
+import { createApp } from "./app.ts";
+import { database } from "./database.ts";
+if (Deno.env.get("PGUSER") !== "places_reader") throw new Error("Server requires places_reader");
+const port = Number(Deno.env.get("PORT") ?? "4000");
+if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error("Invalid PORT");
+const sql = database();
+// Explicit files only: request paths are never translated to filesystem paths.
+const assets = {
+ "/": { body: await Deno.readTextFile("public/index.html"), type: "text/html" },
+ "/app.js": { body: await Deno.readTextFile("public/app.js"), type: "application/javascript" },
+ "/style.css": { body: await Deno.readTextFile("public/style.css"), type: "text/css" },
+};
+await sql`SELECT 1`;
+const controller = new AbortController();
+const stop = () => controller.abort();
+const signals = ["SIGINT", "SIGTERM"] as const;
+for (const signal of signals) Deno.addSignalListener(signal, stop);
+const app = createApp(sql, assets);
+// Oak's standard handle() API keeps middleware native while Deno owns HTTP shutdown.
+const server = Deno.serve(
+ {
+ hostname: "127.0.0.1",
+ port,
+ signal: controller.signal,
+ onListen: () => console.log("Nearby Places listening on loopback"),
+ },
+ async (request, info) =>
+ await app.handle(request, info.remoteAddr) ?? new Response(null, { status: 404 }),
+);
+try {
+ await server.finished;
+} finally {
+ for (const signal of signals) Deno.removeSignalListener(signal, stop);
+ await sql.end({ timeout: 5 });
+}
diff --git a/applications/nearby-places/test/browser.mjs b/applications/nearby-places/test/browser.mjs
new file mode 100644
index 00000000..6aff7c25
--- /dev/null
+++ b/applications/nearby-places/test/browser.mjs
@@ -0,0 +1,179 @@
+import assert from "node:assert/strict";
+import process from "node:process";
+import { spawn } from "node:child_process";
+import { once } from "node:events";
+import { mkdir } from "node:fs/promises";
+import { setTimeout as delay } from "node:timers/promises";
+import { chromium } from "playwright";
+const source = process.env.APP_DIR;
+const evidence = process.env.EVIDENCE_DIR;
+assert(source && evidence, "APP_DIR and EVIDENCE_DIR are required");
+await mkdir(evidence, { recursive: true });
+const env = {};
+for (
+ const key of [
+ "HOME",
+ "PATH",
+ "PGHOST",
+ "PGPORT",
+ "PGDATABASE",
+ "PGUSER",
+ "PGPASSWORD",
+ "PGSSLROOTCERT",
+ ]
+) env[key] = process.env[key];
+env.PORT = "4082";
+let server;
+let serverOutput = "";
+async function start() {
+ serverOutput = "";
+ server = spawn("bash", ["scripts/run.sh"], {
+ cwd: source,
+ env,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+ server.stdout.on("data", (data) => {
+ serverOutput += data.toString();
+ });
+ server.stderr.on("data", (data) => {
+ serverOutput += data.toString();
+ });
+ for (let attempt = 0; attempt < 100; attempt++) {
+ if (server.exitCode !== null) throw new Error(`Server exited: ${serverOutput}`);
+ try {
+ if ((await fetch("http://127.0.0.1:4082/health")).status === 200) return;
+ } catch { /* Startup is not yet ready. */ }
+ await delay(100);
+ }
+ throw new Error("Server did not become ready");
+}
+async function stop() {
+ if (!server || server.exitCode !== null || server.signalCode !== null) return;
+ const exited = once(server, "exit");
+ server.kill("SIGTERM");
+ const timeout = setTimeout(() => server.kill("SIGKILL"), 10_000);
+ const [code, signal] = await exited;
+ clearTimeout(timeout);
+ assert.notEqual(signal, "SIGKILL", "Graceful stop timed out");
+ assert.equal(code, 0, serverOutput);
+}
+let browser;
+let checks = 0;
+async function check(name, fn) {
+ await fn();
+ checks++;
+ console.log(`PASS ${checks}: ${name}`);
+}
+const url = "http://127.0.0.1:4082";
+try {
+ await start();
+ browser = await chromium.launch({ headless: true });
+ const page = await browser.newPage({ viewport: { width: 1440, height: 1100 } });
+ const errors = [];
+ page.on("pageerror", (error) => errors.push(error.message));
+ await page.goto(url);
+ await check("initial synthetic directory is ordered by exact distance and ID", async () => {
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("5 sample places")
+ );
+ assert.deepEqual(await page.locator("#results h3").allTextContents(), [
+ "Anchor Cafe",
+ "Courtyard Cafe",
+ "Garden Library",
+ "Quay Park",
+ "Further Cafe",
+ ]);
+ assert((await page.locator("body").innerText()).includes("Synthetic directory"));
+ await page.screenshot({ path: `${evidence}/desktop.png`, fullPage: true });
+ });
+ await check("category and limit controls filter the real API results", async () => {
+ await page.getByLabel("Category", { exact: true }).selectOption("cafe");
+ await page.getByRole("button", { name: "Find nearby places" }).click();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("3 sample places")
+ );
+ assert.equal(await page.locator("#results h3").count(), 3);
+ await page.getByLabel("Maximum results").fill("1");
+ await page.getByRole("button", { name: "Find nearby places" }).click();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("1 sample place")
+ );
+ assert.equal(await page.locator("#results h3").first().innerText(), "Anchor Cafe");
+ });
+ await check("dateline preset returns the opposite side within 300 meters", async () => {
+ await page.getByLabel("Maximum results").fill("20");
+ await page.getByRole("button", { name: "Across dateline" }).click();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("2 sample places")
+ );
+ assert.deepEqual(await page.locator("#results h3").allTextContents(), [
+ "Dateline East Cafe",
+ "Dateline West Park",
+ ]);
+ assert.equal(await page.locator(".distance strong").nth(1).innerText(), "222.6 m");
+ });
+ await check("empty result and mobile layout remain usable", async () => {
+ await page.getByLabel("Category", { exact: true }).selectOption("library");
+ await page.getByRole("button", { name: "Find nearby places" }).click();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("No sample places")
+ );
+ assert.equal(await page.locator("#results h3").count(), 0);
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.getByRole("button", { name: "London sample" }).click();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("5 sample places")
+ );
+ assert(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
+ await page.screenshot({ path: `${evidence}/mobile.png`, fullPage: true });
+ });
+ const canonical = `${url}/api/nearby?longitude=0&latitude=0&radiusMeters=1200`;
+ let before;
+ await check(
+ "native HTTP rejects malformed and SQL-shaped values and exposes no mutation route",
+ async () => {
+ for (
+ const suffix of [
+ "&category=cafe%27%20OR%20true--",
+ "&longitude=1",
+ "&limit=51",
+ "&unexpected=1",
+ ]
+ ) assert.equal((await fetch(canonical + suffix)).status, 400);
+ for (
+ const longitude of ["NaN", "Infinity", "181", "0%3BDROP%20TABLE%20nearby_places.places"]
+ ) {
+ assert.equal(
+ (await fetch(`${url}/api/nearby?longitude=${longitude}&latitude=0&radiusMeters=10`))
+ .status,
+ 400,
+ );
+ }
+ assert.equal((await fetch(`${url}/api/nearby`, { method: "POST" })).status, 405);
+ assert.equal((await fetch(`${url}/api/nearby?x=${"a".repeat(2100)}`)).status, 414);
+ const response = await fetch(canonical);
+ assert.equal(response.status, 200);
+ before = await response.json();
+ assert.equal(before.places.length, 3);
+ },
+ );
+ await check(
+ "confirmed process exit and restart retain the same seeded Cloud results",
+ async () => {
+ const oldPid = server.pid;
+ await stop();
+ await start();
+ assert.notEqual(server.pid, oldPid);
+ assert.deepEqual(await (await fetch(canonical)).json(), before);
+ await page.reload();
+ await page.waitForFunction(() =>
+ document.querySelector("#status").textContent.startsWith("5 sample places")
+ );
+ assert.deepEqual(errors, []);
+ },
+ );
+ console.log(`Browser/HTTP acceptance: ${checks} checks passed; no browser errors`);
+} finally {
+ if (browser) await browser.close();
+ await stop();
+}
diff --git a/applications/nearby-places/test/cloud_test.ts b/applications/nearby-places/test/cloud_test.ts
new file mode 100644
index 00000000..aae4e073
--- /dev/null
+++ b/applications/nearby-places/test/cloud_test.ts
@@ -0,0 +1,168 @@
+import assert from "node:assert/strict";
+import postgres from "postgres";
+import { database, databaseOptions } from "../src/database.ts";
+import { nearbyQuery } from "../src/places.ts";
+import type { Search } from "../src/input.ts";
+const search = (
+ longitude = 0,
+ latitude = 0,
+ radiusMeters = 1200,
+ extra: Partial = {},
+): Search => ({ longitude, latitude, radiusMeters, limit: 20, ...extra });
+async function withDatabase(fn: (sql: ReturnType) => Promise) {
+ const sql = database();
+ try {
+ await fn(sql);
+ } finally {
+ await sql.end({ timeout: 5 });
+ }
+}
+Deno.test("geography meters, zero distance and independently known equatorial distances", async () => {
+ await withDatabase(async (sql) => {
+ const places = await nearbyQuery(sql, search());
+ assert.deepEqual(places.map((p) => p.id), [6, 8, 7]);
+ assert.equal(places[0].distanceMeters, 0);
+ assert(Math.abs(places[2].distanceMeters - 1113.194908) < 0.1);
+ assert(Math.abs(places[1].distanceMeters - 1105.742758) < 0.1);
+ console.log(
+ "Equatorial meters:",
+ places.map(({ id, distanceMeters }) => ({ id, distanceMeters })),
+ );
+ const zero = await nearbyQuery(sql, search(0, 0, 0));
+ assert.deepEqual(zero.map((p) => p.id), [6]);
+ });
+});
+Deno.test("radius inclusion/exclusion and category filtering use exact geography predicates", async () => {
+ await withDatabase(async (sql) => {
+ assert.deepEqual((await nearbyQuery(sql, search(0, 0, 1110))).map((p) => p.id), [6, 8]);
+ assert.deepEqual((await nearbyQuery(sql, search(0, 0, 1114))).map((p) => p.id), [6, 8, 7]);
+ assert.deepEqual(
+ (await nearbyQuery(sql, search(0, 0, 1200, { category: "cafe" }))).map((p) => p.id),
+ [7],
+ );
+ assert.equal((await nearbyQuery(sql, search(0, 0, 1, { category: "park" }))).length, 0);
+ });
+});
+Deno.test("longitude precedes latitude and equal distances use stable ID ordering/limit", async () => {
+ await withDatabase(async (sql) => {
+ const correct = await nearbyQuery(sql, search(12, 45, 0));
+ assert.equal(correct[0].id, 11);
+ assert.equal(correct[0].longitude, 12);
+ assert.equal(correct[0].latitude, 45);
+ assert.equal((await nearbyQuery(sql, search(45, 12, 1000))).length, 0);
+ const ties = await nearbyQuery(sql, search(-0.12, 51.5, 0));
+ assert.deepEqual(ties.map((p) => p.id), [1, 4]);
+ assert.deepEqual(
+ (await nearbyQuery(sql, search(-0.12, 51.5, 0, { limit: 1 }))).map((p) => p.id),
+ [1],
+ );
+ const again = await nearbyQuery(sql, search(-0.12, 51.5, 0));
+ assert.deepEqual(again, ties);
+ });
+});
+Deno.test("antimeridian proximity and polar coordinates remain geographic", async () => {
+ await withDatabase(async (sql) => {
+ const crossing = await nearbyQuery(sql, search(179.999, 0, 300));
+ assert.deepEqual(crossing.map((p) => p.id), [9, 10]);
+ assert(Math.abs(crossing[1].distanceMeters - 222.638982) < 0.1);
+ assert.deepEqual((await nearbyQuery(sql, search(179.999, 0, 200))).map((p) => p.id), [9]);
+ assert.equal((await nearbyQuery(sql, search(0, 90, 200)))[0].id, 12);
+ console.log("Antimeridian distanceMeters:", crossing[1].distanceMeters);
+ });
+});
+Deno.test("runtime role stays read-only even if its session read-only default is disabled", async () => {
+ await withDatabase(async (sql) => {
+ assert.equal((await sql`SELECT current_user AS role`)[0].role, "places_reader");
+ assert.equal(
+ (await sql`SHOW default_transaction_read_only`)[0].default_transaction_read_only,
+ "on",
+ );
+ await sql`SET default_transaction_read_only = off`;
+ for (
+ const statement of [
+ "INSERT INTO nearby_places.places SELECT * FROM nearby_places.places WHERE false",
+ "UPDATE nearby_places.places SET name='changed' WHERE false",
+ "DELETE FROM nearby_places.places WHERE false",
+ "CREATE TABLE nearby_places.forbidden(id int)",
+ "CREATE SCHEMA runtime_forbidden",
+ "CREATE TEMP TABLE runtime_forbidden(id int)",
+ "UPDATE nearby_places.schema_migrations SET version=version WHERE false",
+ ]
+ ) {
+ await assert.rejects(
+ sql.unsafe(statement),
+ (e: unknown) => (e as { code?: string }).code === "42501",
+ );
+ }
+ assert.equal(
+ (await sql`SELECT count(*)::integer AS count FROM nearby_places.places`)[0].count,
+ 12,
+ );
+ });
+});
+Deno.test("Cloud CA and hostname controls reject wrong trust and wrong hostname", async () => {
+ const options = databaseOptions();
+ const dir = await Deno.makeTempDir();
+ try {
+ const output = await new Deno.Command("openssl", {
+ args: [
+ "req",
+ "-x509",
+ "-newkey",
+ "rsa:2048",
+ "-nodes",
+ "-keyout",
+ `${dir}/key.pem`,
+ "-out",
+ `${dir}/ca.pem`,
+ "-days",
+ "1",
+ "-subj",
+ "/CN=unrelated-test-ca",
+ ],
+ stdout: "null",
+ stderr: "null",
+ }).output();
+ assert(output.success);
+ for (
+ const [ssl, expected] of [
+ [
+ { ...options.ssl, ca: await Deno.readTextFile(`${dir}/ca.pem`) },
+ /CERT|issuer|certificate|UnknownIssuer|CaUsedAsEndEntity/i,
+ ],
+ [
+ { ...options.ssl, servername: "wrong-hostname.example.test" },
+ /ALTNAME|hostname|certificate|NotValidForName/i,
+ ],
+ ] as const
+ ) {
+ const bad = postgres({ ...options, ssl, max: 1 });
+ try {
+ await assert.rejects(bad`SELECT 1`, (e: unknown) => {
+ const error = e as { code?: string; message: string };
+ console.log("Negative TLS control", error.code ?? "no-code", error.message.split(":")[0]);
+ return expected.test(`${error.code} ${error.message}`);
+ });
+ } finally {
+ await bad.end({ timeout: 1 });
+ }
+ }
+ } finally {
+ await Deno.remove(dir, { recursive: true });
+ }
+ await withDatabase(async (sql) =>
+ assert.equal((await sql`SELECT 1 AS positive_control`)[0].positive_control, 1)
+ );
+});
+Deno.test("real EXPLAIN uses the exact application query without forcing an index", async () => {
+ await withDatabase(async (sql) => {
+ const query = nearbyQuery(sql, search(-0.12, 51.5, 1000));
+ const rows = await sql`EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) ${query}`;
+ const plan = rows[0]["QUERY PLAN"];
+ const text = JSON.stringify(plan, null, 2);
+ assert(/st_dwithin/i.test(text));
+ const directory = Deno.env.get("EVIDENCE_DIR");
+ if (directory) await Deno.writeTextFile(`${directory}/explain.json`, text + "\n");
+ console.log("Actual plan:", text);
+ });
+});
diff --git a/applications/nearby-places/test/domain_test.ts b/applications/nearby-places/test/domain_test.ts
new file mode 100644
index 00000000..a91c9e59
--- /dev/null
+++ b/applications/nearby-places/test/domain_test.ts
@@ -0,0 +1,50 @@
+import assert from "node:assert/strict";
+import { InputError, parseSearch } from "../src/input.ts";
+const params = (extra = "") =>
+ new URLSearchParams(`longitude=0&latitude=0&radiusMeters=1200${extra}`);
+Deno.test("finite coordinate bounds, meters and bounded default limit", () => {
+ assert.deepEqual(parseSearch(params()), {
+ longitude: 0,
+ latitude: 0,
+ radiusMeters: 1200,
+ limit: 20,
+ });
+ assert.equal(
+ parseSearch(new URLSearchParams("longitude=-180&latitude=90&radiusMeters=0&limit=50"))
+ .longitude,
+ -180,
+ );
+});
+Deno.test("nonfinite, out-of-range, malformed, duplicate and extra parameters fail", () => {
+ for (
+ const query of [
+ "longitude=NaN&latitude=0&radiusMeters=10",
+ "longitude=1e999&latitude=0&radiusMeters=10",
+ "longitude=181&latitude=0&radiusMeters=10",
+ "longitude=0&latitude=-91&radiusMeters=10",
+ "longitude=0&latitude=0&radiusMeters=-1",
+ "longitude=0&latitude=0&radiusMeters=50001",
+ "longitude=&latitude=0&radiusMeters=10",
+ "longitude=0x10&latitude=0&radiusMeters=10",
+ ]
+ ) assert.throws(() => parseSearch(new URLSearchParams(query)), InputError);
+ for (
+ const extra of [
+ "&limit=0",
+ "&limit=51",
+ "&limit=2.5",
+ "&category=unknown",
+ "&category=cafe%27%20OR%20true--",
+ "&longitude=1",
+ "&unexpected=1",
+ ]
+ ) assert.throws(() => parseSearch(params(extra)), InputError);
+});
+Deno.test("valid optional category and explicit integer limit retain input order", () => {
+ assert.deepEqual(
+ parseSearch(
+ new URLSearchParams("longitude=12&latitude=45&radiusMeters=100&category=library&limit=2"),
+ ),
+ { longitude: 12, latitude: 45, radiusMeters: 100, category: "library", limit: 2 },
+ );
+});