Lokal
Find what you need nearby, and know who is open right now.
Web · iOS · Android

Search 
Map 
Business profile 
Seller dashboard
In Mauritius, “who has this, and are they open?” is usually answered by a phone call, a social page nobody has updated, or a drive to go and see. Small businesses have the stock and the hours; what they lack is a place to say so where people actually search. Lokal is that place — and it never touches the money between a customer and a business.
Lokal is StyloTech’s own product. We define, design, build, run and support it — the React web client, the Flutter app for iOS and Android, the Supabase schema and its row-level security, the Edge Functions and the deployment.
Lokal charges businesses a flat monthly plan and takes no commission, so it has to be cheap to run and hard to misuse: authorisation lives in Postgres rather than in the screens, and both clients answer to the same rules.
- App Store
- Google Play
Neither listing is published yet. The app is built and running on both platforms, and the web version is live now.
- Web
- iOS
- Android
- Web client
- React 19
- TypeScript
- Vite
- Tailwind CSS
- TanStack Query
- Zustand
- Mobile client
- Flutter
- BLoC
- go_router
- get_it
- flutter_map
- Backend
- Supabase
- PostgreSQL
- Row Level Security
- Edge Functions
- pg_cron
- Infrastructure
- Supabase Realtime
- Firebase Cloud Messaging
- OpenStreetMap
- nginx
- Quality & delivery
- Vitest
- oxlint
- GitHub Actions
- Product strategy
- UI/UX design
- Design system
- Technical architecture
- Frontend development
- Backend development
- Mobile development
- Database design
- Integrations
- Testing & QA
- Deployment
- Maintenance
What it does
Each part was built for a real job on the day, not for a feature list. The product’s own site walks through them in full.
Search by what you need
A search for “brochette” or “mechanic” matches what businesses sell, not only what they are called — weighted in Postgres across names, tags, descriptions and products, and kept current by triggers.
Who can serve you now
Open, closed, closing soon, busy, delivery available. A status the seller sets expires after 12 hours and falls back to their opening hours, always read in Mauritius time rather than the device clock.
A map of what is near
Every published business pinned on OpenStreetMap, with the distance from the visitor and the same “can serve me now” filter as the list.
Profiles and catalogues
Photos, opening hours, contact links and a product catalogue with stock status and fixed or “from” prices — the web page a small business would otherwise not have.
Chat instead of a phone call
Customer-to-business messaging in real time, with typing indicators, sent and seen receipts and attachments, and WhatsApp one tap away.
Orders on the Advanced plan
Cart, checkout and an order lifecycle from pending to completed. The buyer pays the seller directly by Juice and uploads the proof; the seller can switch ordering off at any moment.
Seven-step seller onboarding
Business details, contact, social links, products and a live preview of the public profile, with a completion rule that decides when the listing can be submitted.
Activation checked by a person
Sellers pay their plan by Juice against a generated reference. An operator approves or rejects it, approval extends the subscription, and a nightly job unpublishes lapsed listings.
Reviews and saved businesses
Star ratings with seller replies, recalculated by the database, and a saved list for the businesses a customer keeps coming back to.
Insights for sellers
Profile views and orders charted over time, so a seller can see whether being listed is working.
An operator console
Moderation, activations, categories, invitations and free trials, platform settings, seller support and a contact inbox, on a dashboard of its own.
Bilingual and notified
English and French throughout, push notifications written in each recipient’s own language, light and dark themes, and text that scales up to 2.5× on mobile.
One order, traced
A customer finds a shop on Lokal and orders from it. This is everything between pressing “Place order” and the seller’s phone lighting up.
Read from orders.service.ts, migrations 033 and 036 and the push-dispatch Edge Function. Every name below appears in the source exactly as written.
- 01
Checkout needs a signed-in buyer
The checkout page sits behind the authentication guard, and the delivery details are validated by a Zod schema before anything is sent.
- RequireAuth
- checkoutSchema
What it says when it refuses
An anonymous visitor is sent to sign in first; a name under two characters or a phone number under six never leaves the form.
- 02
The client adds up; the database decides
usePlaceOrder hands the basket to the orders service, which totals the lines and prepares the write. The total is a convenience — whether the order may exist at all is not the client’s call.
- usePlaceOrder
- orderItemsTotal
- placeOrder
- 03
Two tables, prices snapshotted
The order is written as pending, in rupees, with payment marked direct — Lokal holds no money. Each line keeps its own name and unit price, so a later price change never rewrites a past order.
- orders
- order_items
- payment_method: 'direct'
- 04
Row-level security checks the buyer
The insert policy requires that the buyer on the row is the signed-in user, and that the business is published.
- buyer_id = auth.uid()
- business_is_public
What it says when it refuses
Ordering as someone else, or from a listing that is not active, is refused by Postgres itself — whatever the client sends.
- 05
…and then the business
The same policy asks whether this business takes orders at all: its switch is on, it is on the Advanced plan and its subscription is active.
- business_accepts_orders
- plan = 'advanced'
- subscription_status
What it says when it refuses
A Basic listing, a lapsed subscription or a seller who has just switched orders off refuses the insert — even from a browser tab opened before the change.
- 06
The seller is told, in their language
A trigger on the new order queues a notification for the seller, rendered from notification_copy in the language on their profile, with English as the fallback.
- queue_order_notification
- notify_user
- notification_copy
- 07
Delivery that survives a failure
The outbox row nudges the push-dispatch Edge Function at once, and a cron job drains anything left every minute. Each row is claimed once, sent to every one of the seller’s devices over FCM and retried up to five times.
- notification_outbox
- push-dispatch
- push-dispatch-drain
- 08
Both sides stay in step
Orders are published over Realtime, so the seller’s list updates live. When the buyer uploads their Juice proof, its timestamp comes from the database clock, not from the phone.
- supabase_realtime
- orders_stamp_payment_proof
Engineering detail
Taken from the project source, not from marketing material.
- Data model
- Supabase Postgres: 21 tables, every one under row-level security with 84 policies between them, 63 SQL functions, 32 triggers and 12 enums, built up across 38 migrations
- Authorisation
- Enforced in the database, not the screens — a seller cannot publish their own listing, only an admin-guarded function can; security-definer functions pin their search path; the service-role key never reaches a client
- Web client
- React 19 with strict TypeScript and Vite, TanStack Query, Zustand, React Hook Form and Zod; 15 feature modules split into domain, application, infrastructure and presentation layers; 58 lazy-loaded routes behind 4 guards
- Mobile client
- Flutter with BLoC, go_router and get_it on the same Supabase project and the same copy — 154 hand-written Dart files across 15 features, with flutter_map, geolocator, image capture and Firebase Cloud Messaging
- Notifications
- A notification outbox with idempotent claiming and up to five attempts, nudged by a trigger and drained every minute by pg_cron; the contact form is rate-limited to three messages per 15 minutes
- Quality gates
- 279 Vitest tests on the web client and 16 Flutter test files; strict TypeScript and oxlint clean; every push to main is type-checked, tested, built and released atomically, then smoke-tested against the live site
Find what you need nearby, and know who is open right now.
