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.
Timeline
Click a stop to open the chapterReal 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.
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.
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.
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.
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.
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.
Milestones
What broke
From the repo
// 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);