Writing

One Next.js app, four surfaces, four languages

Jul 9, 2026 · 3 min read

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 slug from workspace_slug cost 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:

  1. The app's own copy (buttons, emails, error states) is classic i18n, handled with locale routing and message files, German as the default.
  2. 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.