Back to index
01Web App

Harakeh

A live English/Arabic city guide that helps people in Amman find things to do — cafés, padel, karting, escape rooms and more — filtered by budget, across 106 real places on a live map. Sign in with Google to keep a Saved list. It runs on Cloudflare Workers with Supabase and Clerk, and passed a security review with no critical or high-risk findings.

RoleDesign, build, deploy
StartedSep 2026
StatusLive — 106 places

Timeline

Click a stop to open the chapter
Sep 2026

Real places

Started by replacing placeholder content with 45 real Amman venues, then grew the catalogue to 106 places across sports, food and drink, entertainment, outdoors and workshops. Every place carries opening hours, a starting price, coordinates, and its text in both English and Arabic.

—One data file is the single source for every screen
—A validation script checks the catalogue before anything ships
—Budget filter works off real starting prices, not categories
Sep 2026

Live map

Split the app into one module per concern and added a Map tab built on MapLibre GL, with custom pins per activity and the user's location as an optional extra rather than a requirement.

—Fixed iOS and iPad sizing bugs in the map view
—Location is asked for from a tap, with clear steps if it was blocked
—In-app browsers (where location can't work) are detected and explained
Sep 2026

Accounts + Saved

Added sign-in through Clerk with Google, and a Saved list backed by a small API on the Cloudflare Worker that reads and writes Supabase Postgres. Every query is scoped to the signed-in user, row-level security is on, and requests are rate-limited through Upstash Redis.

—Saved API runs at the edge on the same Worker that serves the app
—Secrets live in the Worker's runtime settings, never in the repo
—Account deletion cleans up the user's saved rows through a webhook
Sep 2026

Security review

Ran a full security review before launch: no critical or high-risk findings. Then fixed the remaining findings: rate limiting now runs before authentication, unknown API routes return 404 without touching the auth provider, and provider error messages no longer reach the logs.

—Full git history scanned — no secret ever committed
—No IDOR: every database query is filtered by the user's id
—Consistent output escaping and strict security headers on every response
19 Sep 2026

Launch

Launched on harakeh.mkll.me with a production Clerk instance and my own Google OAuth credentials. The old workers.dev address now permanently redirects to the real domain. The desktop and right-to-left Arabic layouts were rebuilt properly, and the privacy policy and terms exist in both languages.

—Push to master deploys automatically through Cloudflare Workers Builds
—No analytics of any kind, as the privacy policy states
—Arabic direction is inherited from the document, using logical CSS properties
Now

Tester feedback

Testers are using it, and their bug reports drive the work: a map pill covering the place card, accidental zoom on taps, the keyboard hiding filter options, and iOS Safari asking for location again on every visit — all fixed.

—Next: draw the 106 map pins as a MapLibre symbol layer instead of DOM elements, to remove map lag

Milestones

What broke

The Saved list returned 503 in production
Three separate causes stacked up: secrets had been set in the build settings instead of the Worker's runtime settings, a transposed Supabase project reference triggered Cloudflare error 1016, and a new Supabase project hadn't granted table privileges to the service role. Each fix is now written into the config or the migration.
Account deletion silently never ran
The webhook library's v2 verify() only validates and returns nothing, but the handler read its return value, so every user-deleted event crashed and retried into the same crash. The handler now parses the body itself after verifying, and a regression test pins that behaviour.
"I can't sign in" after launch
Instead of guessing, I tailed the Worker's live logs: the requests were arriving on the old workers.dev address from a saved bookmark, which the production auth provider doesn't trust. A permanent redirect to the real domain fixed it for everyone.

From the repo

functions/api/[[path]].js (condensed)
// unknown routes never reach the auth provider
if (!ALLOWED_ROUTES.some((r) => r.test(pathname))) {
  return json({ error: "Not found." }, 404);
}

// rate-limit by IP *before* authenticating
const limited = await enforceIpLimit(env, request, "auth");
if (limited) return limited;

const userId = await authenticate(request, env);
if (!userId) return json({ error: "Unauthorized." }, 401);

// every query below is scoped to this user
return saved(request, env, userId);
FrontendJavaScript, MapLibre GL
EdgeCloudflare Workers
AuthClerk, Google sign-in
DataSupabase Postgres, row-level security
Rate limitUpstash Redis
DeployPush to master → Workers Builds
Next project
Homelab
Open →