DineEasy is a Swiss restaurant ordering platform I built solo as a contractor for Maxapp GmbH. The part of its architecture worth writing about isn't any single feature. It's the constraint: one Next.js app serves the marketing site, authentication, the restaurant owner's dashboard, and every restaurant's public-facing menu.
The subdomain trick
Each restaurant gets its own menu at {slug}.dineeasy.app. There's no per-restaurant deployment and no separate menu app. A proxy layer catches the wildcard subdomain and rewrites it into a route the app already knows how to render, so pizzeria-luigi.dineeasy.app and an internal path like /r/pizzeria-luigi are the same page wearing different clothes.
Two lessons from running this in production:
- Keep two slugs. The public customer URL and the dashboard workspace URL are different namespaces. Restaurants rename their public presence; you don't want that to break every internal link and bookmark their staff has. Separating
slugfromworkspace_slugcost one column and saved real pain. - Local dev needs an escape hatch. There are no wildcard subdomains on localhost, so the same page is reachable at a plain path in dev. If a routing trick only works in production, you'll find its bugs in production too.
Four languages is not an i18n checkbox
Switzerland has four national languages, and a restaurant in Zürich genuinely gets diners who expect German, French, Italian, and English. Two very different problems hide in that sentence:
- The app's own copy (buttons, emails, error states) is classic i18n, handled with locale routing and message files, German as the default.
- The restaurant's content (menu items, descriptions) is user-generated and can't ship in a translations file. DineEasy stores menu content as multilingual structured data and machine-translates it with DeepL, so an owner writes their menu once in German and a French-speaking diner still reads it natively.
Treating those as one problem is how you end up with either untranslated menus or a CMS nobody fills in.
Money and trust boundaries
Payments run on Stripe Connect with direct charges: diners pay the restaurant's own Stripe account, not a pooled platform account, which keeps the money flow and the liability where they belong.
On the data side, every table sits behind Postgres row-level security in Supabase. RLS is the kind of thing that feels like bureaucracy until the first time a dashboard query bug would have leaked another restaurant's orders and didn't. For a multi-tenant app written by one person, the database enforcing tenancy is worth more than any amount of careful application code.
The supporting cast follows the same one-person-ops philosophy I wrote about with Lensdrop: Upstash Redis as a cache and rate limiter, QStash for background jobs and scheduled work, everything serverless so there's no machine to babysit.
What I'd tell past me
Solo-building a product with four surfaces means every architectural shortcut multiplies. The single-app constraint looked like a limitation at the start and turned out to be the reason one contractor could ship and operate the whole thing: one build, one deployment, one set of conventions, and a routing layer clever enough to make it look like four products.