In Active Development

Lokal — Multi-Tenant Cafe Operating System

A business platform unifying loyalty programs, POS, and store management into a single ecosystem.

FlutterRiverpod.NET 10PostgreSQLRedisSignalRDapperDocker
The Problem

Every cafe in Turkey runs loyalty differently. Stamp cards that get lost in wallets, apps that nobody downloads twice, punch cards from 2003 — fragmented and forgettable. For the customer, it's ten cafes, ten systems, none of them talking to each other.

For the business owner, it's worse. They need a POS system, a loyalty program, staff management, inventory tracking — and they're duct-taping four different tools together. The barista at 8am with a line out the door shouldn't need to think about which app does what.

The Solution

Lokal is one platform that does all of it. Three apps — one for the customer, one for the business, one for admin — sharing a common core. The customer sees one loyalty card per business. The business gets a full POS, menu management, staff roles, and real-time reporting.

Screenshot 1
Screenshot 2
Architecture & Decisions

Modular monolith .NET 10 backend, Flutter monorepo with 3 apps (lokal-common shared package), PostgreSQL database, and Redis cache. Real-time SignalR hub.

System architecture diagram

Why Scoped RBAC?

Generic RBAC doesn't distinguish between 'can manage this store's orders' and 'can manage all stores.' I encoded the scope directly into permission keys — Store.Order.Create vs Business.Order.Read. One lookup, scope inferred from the key.

Why Business Template → Store Override?

The hard part wasn't building the menu — it was making it work when a business has 5 stores that each charge different prices for the same espresso, while still generating unified reports. Products are defined once at the business level, each store can override the price.

Why Order Price Snapshotting?

When a customer buys an espresso for 3.50, that price is frozen in the order forever — even if the menu changes tomorrow. Historical orders are immutable, reports show what customers actually paid.

Feature Highlights

40+ Granular Permissions

A Barista sees different things than a General Manager. 7 default roles, custom role creation, and Business/Store scoped permissions ensure every employee accesses only what they need.

Dynamic Store Pricing

Products defined once, each store sets its own price. Store-level flexibility without breaking business-wide reporting.

Multi-App Ecosystem

Customer, business, and admin apps share a common core via the lokal-common package, while each maintains its own user experience.

Challenges & Lessons

Permission System Redesign

The first auth system was too rigid — hardcoded roles, non-extensible permissions. The moment it blocked businesses from creating their own roles, I redesigned the entire system from scratch. Scope-encoded permission keys, dynamic roles, and representative overrides. This was the single biggest rewrite in the project and one of the most critical design decisions I've made.

Catalog Price Resolution

Getting the store override layer right was harder than expected. Every query had to account for both business defaults and store overrides. In the first implementation, distinguishing between NULL overrides and 'no price' was a problem. Once I landed on the COALESCE pattern, everything clicked — but the path there involved several false starts.

Outcomes & Impact
3

Production Flutter apps (Customer, Business, Admin)

40%

Faster query execution with Dapper (~45ms vs EF Core ~80ms)

40+

Granular permission keys with 7 default roles

Live

Deployed as FAB Coffee on App Store and Play Store