Social & Marketplace

Tomogi

Hyperlocal Social Network and Neighbourhood Task Platform

A neighbourhood platform where people post local tasks, neighbours take them on, and completed work builds a reputation that is earned rather than claimed. Web, plus Flutter applications for Android and iOS.

Industry
Social Networking / Local Services
Location
Global
The problem

Neighbourhood help already happens, badly. It is scattered across group chats, community boards and local social groups, where requests scroll away within hours, there is no way to tell who is reliable, and someone who has helped a dozen neighbours has nothing to show for it. The people most willing to help are the hardest to find.

  • Local requests lost in group chats and community feeds within hours
  • No way to judge whether a stranger offering help is trustworthy
  • Reputation earned in one place does not travel or persist
  • Nothing tying a request to the specific area it actually concerns
  • Follower counts rewarded visibility rather than usefulness
What we built

BrainGenz built Tomogi as a location-anchored platform where a request is a mission with a lifecycle rather than a message that scrolls away. Neighbours offer, the creator chooses, completion is recorded, and reputation accumulates from work that actually happened. It runs on the web and on Flutter applications for Android and iOS, all against one API.

  • Modelled missions as a state machine - offered, accepted, active, completed - rather than as posts
  • Anchored the experience to a detected neighbourhood, with a fallback chain so it survives a failing geocoder
  • Built a live radar view mapping nearby needs, offers and events
  • Tied reputation and endorsements to completed missions rather than to follows
  • Kept authorisation, validation and state transitions in the API so web and mobile cannot diverge
  • Split chat into direct and mission threads so a negotiation is not buried in unrelated conversation

How it was built

The product decision that shaped the engineering

Tomogi describes itself as a gig layer for neighbours: post a task, have someone nearby take it on, and build a local reputation from work actually completed. That last clause is the one that constrains everything. A reputation that can be inflated is worth nothing, so the system has to be able to say who did what, and be right about it.

Most of the interesting decisions below follow from that, rather than from anything about social feeds.

Claims became offers, and the model got simpler

The first design let a neighbour claim a mission, which closed it. That reads well and behaves badly: the fastest tap wins rather than the best fit, and a creator who dislikes the claimant has no move except to cancel.

It was reworked so a claim is an offer. Several people can offer on the same mission, the mission stays open, and the creator decides from inside the chat thread with that person - which is where they have the context to decide. Accepting one offer activates it, automatically rejects the others and notifies everyone involved.

The states that mattered were the ones between: pending, active, rejected, and completion gated so that only an active claim can be marked done. Modelling those explicitly removed a category of support question rather than adding features.

A silent failure that looked like a broken button

The most instructive bug on this project presented as "delete does not work". Deleting a profile ran, returned success, and changed nothing.

The cause was two layers away. The authentication routes were calling sign-in against the same shared Supabase client the rest of the backend used for privileged work. The client library stores the session on itself, so from the first login onward that shared client was no longer acting with service-role permission - it was acting as whichever user had logged in most recently. Row-level security then did exactly its job and blocked the write, matching zero rows and reporting no error, because deleting nothing is not an error.

The fix was a dedicated client for authentication flows so the privileged one is never re-scoped, and delete operations changed to return the affected rows and fail loudly when that count is zero. The lesson generalises: a shared client that carries mutable state is a shared global, and a query that silently matches nothing is worse than one that throws.

Location that survives its own dependencies

The whole product is anchored to where you are, so neighbourhood detection is not a nice-to-have. It broke in production when billing lapsed on the Google Cloud project behind the Maps key - geocoding started refusing requests and the feed header froze on a stale neighbourhood.

Rather than wait for an account fix, a fallback chain was built: a saved neighbourhood if the profile has one, stored coordinates if not, live browser geolocation if neither, and OpenStreetMap Nominatim as a keyless geocoder when Google is unavailable. Google remains the primary path because the results are better, but the feature no longer stops when one vendor relationship does.

A related fix was less visible and just as consequential. Onboarding was firing a geocode request and then finalising the profile without waiting for it, so a slow lookup was silently discarded and the user landed with no neighbourhood at all. Finalisation now awaits it with a short cap, and failures log rather than vanish.

One backend, three surfaces

The platform runs a Next.js web application and a Flutter codebase covering Android and iOS against a single Node and Express API, with Supabase underneath for the database, authentication and storage.

The discipline that makes that work is keeping rules in one place. Authorisation, validation and the mission state machine live in the API, not in each client, because three implementations of the same rule become three subtly different rules within a release or two. The clients stay responsible for presentation and for the parts genuinely specific to a platform - native location permissions, deep links, share sheets.

Media is handled server-side for the same reason: images resized on upload and video normalised, so no client needs to be trusted to have done it correctly.

Permissions, stated honestly

A location-first app has to ask for location, and users are right to be careful about that. The settings screen reflects the browser's real permission state rather than a preference the app stores and hopes matches reality, and enabling a permission triggers the genuine system prompt.

One limitation is worth naming because the interface has to be honest about it: a web application can request a permission but cannot revoke one. Turning it off in the app records the intent and points the user at their browser settings, because pretending otherwise would leave someone believing they had withdrawn access they had not.

A rate limit that looked like a broken feature

Mid-testing, adding a business stopped working. No error worth reading, just failure.

The API polls for new activity every few seconds - reasonable for a feed that should feel live - and the default rate limit of one hundred requests per fifteen minutes was consumed by polling alone, leaving nothing for anything a person actually did. The limit was not wrong in principle; it was set for a request pattern the product does not have.

It is a small thing and a good illustration of a general one: infrastructure defaults are chosen for an imagined average application, and the failures they produce rarely announce themselves as configuration.

What we would revisit

The polling. It is the right thing to ship first because it is simple and it works everywhere, and it is the wrong thing to keep as concurrency grows - the cost is paid continuously by every client whether anything changed or not. Supabase already provides a realtime subscription path, and moving the feed and chat onto it is the change with the largest effect on both server load and how immediate the product feels.

Before
  • Requests scrolled out of sight in general-purpose group chats
  • No signal about who could be trusted with a task
  • Helpfulness left no durable record
  • Location handled as free text, if at all
After
  • Missions carry a lifecycle from offer through completion
  • Reputation and endorsements written from completed work
  • Neighbourhood detected automatically, with fallbacks when a provider fails
  • The same rules enforced across web, Android and iOS from one API
Stack
Web
Next.js 14React 18TypeScriptTailwind CSSTanStack QueryMotion
Mobile
FlutterDartGetXProviderGoogle MapsGeolocator
Backend
Node.jsExpressSupabasePostgreSQLSharpFFmpeg
Platform
VercelRailwaySupabase AuthGoogle Maps APIOpenStreetMap