docs
/
Building on Appmint

When a page is not enough

The four signals that you have outgrown a page application, and what the crossing actually costs — from a marketplace built headless over AppEngine.

Everything in this section so far argues that page applications go further than people assume. This page is the other half of that argument: knowing when to stop.

The crossing is expensive and hard to reverse. Make it for a reason.

The four signals

You have outgrown a page when any one of these is true. Not a majority — any one.

1. You need to write repeatedly as the user works

Autosave, drafts, per-step persistence, "save and come back later". The read/write asymmetry means a page can commit through a domain namespace, but it cannot write arbitrary records — and it certainly cannot do so repeatedly as someone works.

This is the most common crossing, and the clearest.

2. A credential must never reach the browser

Anything where the visitor must not be the caller: reading records belonging to someone else, acting on another account's behalf, moving money beyond a cart, or any operation where being able to forge the request is a breach rather than a nuisance.

A page's credentials are, by construction, public.

3. You need routing with real state between views

Not "several pages" — several pages is fine. This is about a flow where view B depends on state established in view A, and a reload in the middle must not lose it. appmint.state is session-only, and the page you navigate to is a fresh document.

4. You depend on a build step

TypeScript, a component library, tests that run against your UI, a design system shared with another product. You can hand-write a large single file — the design studio is 113 KB of it — but you cannot type-check it, unit-test it, or share a component with anything else.

Notice what is not on this list: size, complexity, canvas work, animation, touch gestures, or "it feels like an app". None of those require crossing. The design studio does all of them in a page.

What you get, and what you take on

The worked example is stowbo, a storage-and-parking marketplace: listings, bookings, custody hand-over, host payouts, messaging. It has all four signals — hosts edit listings repeatedly, payouts must not be forgeable, booking spans several views, and it is a TypeScript codebase.

It runs as a Next.js application, headless over AppEngine. AppEngine remains the backend; nothing was forked or replaced.

The shape

  Browser ──▶ Your Next.js app ──▶ AppEngine
           (1)                 (2)

(1) The browser only ever talks to your origin. Same-origin requests, your cookies, your session.

(2) Your server holds the credentials, adds the tenant headers, and decides what the visitor was entitled to ask for.

A catch-all proxy route (src/app/api/[...slug]) forwards approved calls, so the browser never sees an AppEngine host or token.

The session

The customer's token goes in an httpOnly cookie, which the server forwards as x-client-authorization:

export async function setSessionCookies(token: string, refreshToken?: string) {
  const jar = await cookies();
  const secure = process.env.NODE_ENV === "production";
  const opts = { httpOnly: true, secure, sameSite: "lax" as const, path: "/", maxAge: MAX_AGE };
  jar.set("token", token, opts);
  if (refreshToken) jar.set("refreshToken", refreshToken, opts);
}

httpOnly is the whole point: browser JavaScript cannot read it, so an XSS bug cannot exfiltrate the session. The two identities stay distinct — your application authenticates as itself, the visitor rides alongside. See Authentication.

A typed layer over the endpoints

Rather than scattering paths through components, each domain gets a module — stowbo.ts, finance.ts, community.ts, custody.ts, coupons.ts — over one configured client. When an endpoint changes, one file changes.

This is the concrete payoff of signal 4, and it arrives immediately.

What it costs

A deployment. Build, host, monitor, roll back. The page version had none of this.

Environment management. Endpoint, org id, app keys, OAuth credentials, per environment. A page inherits all of it from the host.

Auth you own. Sign-in, refresh, expiry, sign-out, and every "what does this user see" decision. A page gets a session it never has to think about.

Every state a page gave you free. Loading, empty, error and unauthorised, per route, written by you.

Route discipline. A page application cannot link to a route that does not exist. An app can, and will — every redirect target and every button is a 404 waiting to happen unless routes come from one shared definition rather than string literals.

The failure mode of crossing too early is not that the app is worse. It is that you spend the first two weeks rebuilding things the platform was already doing — session handling, cart pricing, empty states — and the actual product is two weeks late.

When the API does not have your endpoint

Going custom does not mean going around the platform. Stowbo needed host-side listing CRUD that did not exist, so those endpoints were added to AppEngine rather than reimplemented in the app.

That is usually the right call:

  • The rule lives where every client sees it — web, mobile, admin
  • No duplicate logic drifting between clients
  • Permissions and validation stay in one place

Reimplementing platform behaviour in your app is how a second, subtly different system gets built without anyone deciding to build one.

The rule that outlasts everything else: the server decides money. Prices, discounts, shipping and tax are computed by AppEngine and rendered verbatim. The cart is the truth — the client presents what is owed and sends payment plus cart to the server. A client that re-derives a total will eventually disagree with the order, and the customer will be right. See Overview.

The honest recommendation

Build it as a page first, unless one of the four signals is already true.

Not because pages are better, but because the signals are hard to predict and cheap to detect. You will know within a week whether you need to write repeatedly or gate a credential — and until you know, the page version is running, in front of people, with no deployment to maintain.

Several things that were going to be applications turned out to be pages. One — stowbo — was an application from the first sketch, because hosts editing their own listings is signal 1 and taking payouts is signal 2, and no amount of cleverness makes those fit.

Next