JOURNAL · JUNE 17, 2026

from Travolp to a white-label kit: the parts that travel

We built Travolp as a multi-tenant travel platform. Here is what we learned packaging the reusable shell into a white-label kit, and how AI-assisted coding got us there.

Published
June 17, 2026
Author
Priorli
Tags
white-label, multi-tenancy, ai-assisted-coding, kotlin-multiplatform

A travel agency signs up, picks a color, and uploads a logo. A week later their customers download an app from the App Store with the agency’s name on it. The app plans trips, works offline, and rewrites a day with AI when the weather turns. Those customers never see the word Travolp.

That is the white-label promise, and we shipped it in Travolp. The interesting part was never the trip planner. It was the shell underneath that lets one codebase become many branded apps with their data kept apart. We are now pulling that shell out of Travolp into a kit we can put under the next product. Here is what we learned building it, and what survives the move.

tenancy is a boundary, not a theme

The first instinct with white-label is to treat it as theming. Swap the logo, change the primary color, ship. That instinct is wrong, and it is the most expensive mistake to unwind later.

A tenant is a security boundary before it is a brand. Two agencies on the same system must never see each other’s trips, customers, or revenue. So tenancy is not a setting painted on at the end. It is the thing every other part of the app is built around.

We kept that in our own hands instead of leaning on the auth provider’s built-in organizations. Clerk answers who you are. Whether you can see a given trip is a separate question, and we wanted the roles, the access rules, and the invite flow under our own control. One person can belong to more than one agency, with a different role in each, and the two never bleed together.

The rule that makes this hold is plain to state and unforgiving in practice. Every read and write is scoped to a single tenant. There is no path that lists across tenants, not even for our own staff. A superadmin who needs to look inside an agency enters through an audited session that drops a red banner on the screen and leaves a trail behind it.

Branding sits on top of that boundary. A request to an agency’s own domain is matched to its tenant in middleware and tagged before any handler runs, so the right data and the right logo are decided in the same place. The brand is downstream of the boundary, not the other way around.

branded apps from a config file

The web side of white-label is straightforward once tenancy is solid. Read the tenant, apply its color and logo, serve its domain. Mobile is where it gets physical, because the App Store wants a real app with a real bundle id, not a runtime theme.

We drive each agency’s build from one file. A tenant config looks like this:

{
  "slug": "acme-tours",
  "name": "Acme Tours",
  "primaryColor": "#1F6E6E",
  "iosBundleId": "com.example.acme",
  "androidAppId": "com.example.acme",
  "deepLinkScheme": "acme"
}

A generator turns that into the assets a build needs. App icons and a splash screen rendered at each density. An Android product flavor with its colors and strings. An iOS xcconfig overlay carrying the bundle id and a TenantSlug entry in Info.plist. One Apple Team signs every agency’s bundle id, so adding an agency does not mean a new developer account or a re-certification.

The app itself is Compose Multiplatform, which I will come back to. What matters for branding is that the only thing it reads at startup is its own slug, through an expect/actual that pulls from BuildConfig on Android and Info.plist on iOS. The slug is not magic. Our own app is just the tenant whose slug happens to be travolp.

one repo, two toolchains

Travolp is one git repository, and the web app and the mobile app inside it run on toolchains that never link to each other.

travolp/
  web/        next.js app: api, dashboard, admin (bun)
  mobile/     compose multiplatform: android + ios (gradle)
  specs/      one markdown file per feature, shared
  branding/   per-tenant config and the asset generator

The web/ side is Next.js on Bun. It serves the dashboard, the admin, and the API under /api/v1. The mobile/ side is Kotlin, built with Gradle. There is no JavaScript workspace tying them together, and no Kotlin dependency on the web code. Each half installs and builds on its own.

What the mobile half shares is internal. A Kotlin Multiplatform module holds the domain models, the use cases, the repositories, and the Ktor client that talks to the API. The Compose UI sits on top of that and runs on both Android and iOS from one source. Platform differences go through expect/actual, and there are fewer of them than you would guess. Most screens are written once.

The harder question is how Kotlin and TypeScript agree on the API, because they cannot share types. So we made the contract explicit instead of inferred. Every endpoint answers in one envelope, { data } on success or { error: { code, message } } on failure, and the shape of each endpoint lives in an OpenAPI document that both clients read. Change an endpoint and the order is fixed: OpenAPI first, then the web client, then the Ktor client.

The repo also carries a deploy boundary. The web image builds with the repository root as its Docker context, but .dockerignore drops mobile/ and specs/, so no Kotlin or spec files land in the server image. Mobile ships its own way through Gradle and Xcode. One repository, one place to change a feature across all three surfaces, and two build pipelines that stay out of each other’s way.

the skeleton, and a skill for each phase

AI-assisted coding falls apart without a skeleton. Point a model at an empty repo and it invents a folder layout, a response format, and a way to handle errors, then invents a slightly different one next week. The fix is to give it a shape to fill in, and to make filling it in the only easy path.

The kit is that shape. Tenancy, auth, billing, the /api/v1 routes, the { data } / { error } envelope, and the design tokens are already in place. A feature does not start from a blank page. It starts as a markdown spec with three sections, API, web, and mobile, each with a checklist of what is done. The conventions live where the model reads them: the response envelope, where endpoints go, and a short list of design rules, like reuse the shared button instead of hand-rolling one.

On top of the skeleton sits a small set of reusable skills, one for each phase of a feature.

Plan turns a request into a spec. It writes the three sections and the checklist, and checks the feature matrix so the same thing is not built twice.

Develop implements one checkbox at a time against that spec, inside the conventions the skeleton already sets. Because the shape is fixed, new code comes out looking like the code already there.

Verify reads the result back and checks it against the spec. It ticks a box only when the code actually satisfies it. This is the step that catches a model reporting a feature as done on mobile when only the web half exists.

Test runs the build and the tests for the surface that changed. A spec is not finished until they pass, and until the handbook, the plain-language description of what the product does, is updated to match.

The skills are reusable because they encode the loop, not the feature. The same four run for a booking flow, a payout report, or a settings toggle, on web or on mobile. That is the part that gets missed about building with AI. It is not a clever prompt. It is a skeleton the model cannot drift out of, and a few skills that make plan, develop, verify, and test the same motion every time.

what travels into the kit

So what actually goes into a white-label kit, once the travel parts are stripped away?

The tenancy models, and the discipline of scoping every query by tenant. The branding pipeline that turns a config file into branded web and mobile builds. Auth, wired to keep identity and tenancy separate. Billing through Stripe, including the split between agencies that collect their own money and agencies we pay out. The config cascade that lets a setting be overridden per tenant or per user. Localization. The repo layout that keeps web and mobile in one tree, joined by shared specs and an OpenAPI contract rather than a shared type package. And the workflow itself: the skeleton, plus a reusable skill for each phase, so plan, develop, verify, and test run the same way on every feature.

The travel domain stays behind. Itineraries, maps, the AI trip-planning features. Those are Travolp. The shell is the kit.

If you take one thing from this, start from the boundary, not the surface. Theming is a weekend. Multi-tenancy that holds up as a security boundary, and a build pipeline that produces real apps from a config file, is the work. Get those right and the logo swap is the easy last step.

working with Priorli

Priorli built Travolp, and we build software like it for other teams. We are a small, senior group. We embed with founders and product teams on greenfield builds, AI features, and codebases that are already a few years deep.

None of the patterns in this post are secret. What takes time is getting the edges right. Data isolation that actually holds. A build pipeline that does not break on the fifth agency. An AI workflow that ships features instead of demos. That is the part you pay for when you hire us, and it is the part that is hard to rebuild from a blog post.

If you are weighing a white-label product, or an app that has to run on web and mobile without a separate team for each, this is the work we do. We will also tell you when it is the wrong call. A white-label build is heavier than a single-tenant one, and plenty of products do not need it on day one.

If it is the right call, book the discovery call or email hello@priorli.com.