# One Next.js app, four surfaces, four languages

How DineEasy serves a marketing site, an owner dashboard, and every restaurant's public menu from a single app, in all four Swiss national languages.

[DineEasy](https://dineeasy.app) 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](/blog/resumable-uploads-to-r2): 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.

Last updated on Jul 10, 2026.