Vitrina is our platform for cafés and restaurants. A venue gets an ordering website under its own name and, as an option, a mobile app, plus an admin panel, a tablet POS, a courier app and a small print agent that sends orders to the kitchen printer. It grew out of the system we built for Pyrizhok, a home-style café in Kharkiv, which is also its first client.
This post is about one architectural decision that shapes everything else: every client runs its own copy of the platform.
One client, one deployment
The obvious way to build software for many restaurants is multi-tenancy: one server, one database, and a tenant ID on every row. We designed that version first, and then dropped it.
In Vitrina, a client is a separate instance of the product. It has its own backend, its own PostgreSQL database, its own domains and its own keys for payments, fiscal receipts, SMS and email. The boundary between two clients is the database connection string, not a column in a shared table.
That removes whole classes of problems instead of guarding against them:
- Data from two venues never meets. No query can leak one café's guests into another café's report, because they are not in the same database.
- A guest belongs to one venue. Loyalty points and order history live in the venue's own database.
- Payments go straight to the venue. Each client connects its own merchant account with LiqPay, monobank, WayForPay or Stripe, so guest money never passes through us.
- Backups, data export and deletion happen per client.
The trade-off is operational: there are more deployments to run, and an update has to roll out to each of them. We accept that because every client runs the same code. There are no client forks.
A brand is configuration, not a fork
Everything that makes one client different from another lives in a brand configuration file, selected by an environment variable. It holds the name (with its grammatical cases, which Ukrainian needs), the domain, the colour palette, fonts, home page layout, icon set, guest languages, currency and its formatting, time zone, country, phone number format, logos and the identity of the mobile apps.
Why in code and not in the database? Because the website and the mobile apps are built before they ever talk to a backend, and colours, icons and app identifiers have to be baked in at build time. Configuration that the build depends on cannot live in a database the build does not connect to.
One rule turned out to matter: the brand must be set explicitly. A deployment with an empty or unknown brand key refuses to start. Otherwise a new client's deployment that forgot one variable would come up under another client's name, and their guests would notice before we did. We also run a check that counts the places where a client's name is written into the code instead of coming from the configuration, and it fails if that number grows.
Locations inside a client
A café with several locations is still one client. Each location is a record in the client's database with its own address, delivery zone, opening hours, stock, till, printers and staff. The menu, guests, loyalty programme, website and app are shared across locations.
Modules and the licence
The core of the platform (menu, cart, orders, payments, guest accounts and settings) is always on. Larger areas are modules: delivery, POS, kitchen printing, fiscal receipts, stock, purchasing, loyalty, marketing, reviews, the AI assistant and others.
Which modules a client has is written in a signed licence token. The server checks the signature with a public key; the private key never leaves us. A module that is switched off answers with a clear "payment required" error instead of a confusing permission error, and the website and app simply do not show its sections.
Languages
The guest side can speak seven languages (Ukrainian, Russian, English, Polish, German, Slovak and Czech), and each brand chooses which of them it offers. Staff apps speak eleven languages, and the language belongs to the person, not the venue: a courier who prefers Romanian sees Romanian, while the guest website of the same café may be only in Ukrainian and English. The translations are machine-made with a glossary and still need review by native speakers.
Who owns what
The platform code is ours. Everything that makes a client a client belongs to the client: the name and logo, the menu, the photos, guest data, the domain and the app store accounts. If a client leaves, their data is exported.
What it costs a venue
Vitrina is a monthly subscription. On the entry plans there is also a small percentage of online orders (at most 1% at the time of writing); the top plans have none, and no plan takes a percentage of sales at the till. Current prices are on vitrina.cafe.
Where it stands today
The platform is built and Pyrizhok's deployment is set up, but the café has not opened it to guests yet. Anyone can try the guest side in the demo café with test money. If you run a café or a small chain and want your own ordering channel, write to us.