Guide · Lovable · TanStack Start

    Lovable TanStack Start app to Android and iOS (2026)

    Nathan Claiborne
    Since May 13, 2026, every new Lovable project is a TanStack Start app: its pages are rendered on a server, and the published site runs on Cloudflare. That is good for the website, but an app on a phone has no server, and the usual Capacitor recipe of pointing it at dist/ finds no index.html there. This guide shows how Corenna builds these apps for Android and iOS, and the one thing to set up if your app uses server functions.

    Which kind of Lovable app do you have?

    • TanStack Start (projects created since May 13, 2026): package.json lists @tanstack/react-start and Lovable's @lovable.dev/vite-tanstack-config, pages live in src/routes/, and there is no index.html in the repo.
    • Vite and React (older projects): an index.html at the top of the repo and a build that writes the site to dist/. These keep working exactly as before; the Lovable, Bolt or v0 to APK guide covers them.

    Not sure? Paste the repo into the free repo check. It names the framework, and for a TanStack Start app it says whether the app needs a hosted backend.

    What Corenna does with a TanStack Start app

    Your website's own build stays as it is. For the app, Corenna makes a second, client-only build in TanStack Start's SPA mode, from a copy of your Vite config made during the build, so nothing in your repo changes. It packages only the files a browser runs, never server code or .env files, and the app opens on the start page that build writes. Capacitor is added at build time as well, so the repo needs no mobile setup of its own.

    If your package.json has a build:mobile script, Corenna runs that instead and packages what it produces.

    Does your app need a server?

    It does if it uses server functions: code wrapped in createServerFn, usually in files named *.functions.ts. Those run on your website's server, and an installed app has none.

    • No server functions: nothing to set up. The app runs from its own files, and calls to Supabase or any other service go straight from the phone, as they do on the website.
    • Server functions: the installed app sends them to your published website. Set that up once, as below.

    If your app uses server functions: set a hosted backend

    1. Publish the app in Lovable. The installed app calls the code that is published, so publish again whenever you change a server function: one the site doesn't have yet fails in the app.
    2. Set the Hosted backend URL on your Corenna project page to the published address, such as https://your-app.lovable.app or your own domain: https, with no path after it.
    3. Let the website accept the app's requests. The installed app calls from its own origin, https://localhost on Android and capacitor://localhost on iOS. Browsers need CORS headers for that, and TanStack Start's CSRF protection turns away other origins by default. This src/start.ts lets both through:
      import { createCsrfMiddleware, createMiddleware, createStart } from "@tanstack/react-start";
      
      // The installed app calls server functions from its own origin:
      // https://localhost on Android, capacitor://localhost on iOS.
      const APP_ORIGINS = ["https://localhost", "capacitor://localhost"];
      
      const corsHeaders = (origin: string) => ({
        "Access-Control-Allow-Origin": origin,
        "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
        "Access-Control-Allow-Headers": "content-type, accept, authorization, x-tsr-serverfn",
        // TanStack Start reads these on every server function response.
        "Access-Control-Expose-Headers": "x-tss-serialized, x-tss-raw, x-tss-context",
        Vary: "Origin",
      });
      
      const appCors = createMiddleware({ type: "request" }).server(async ({ request, next }) => {
        const origin = request.headers.get("Origin");
        const fromApp = origin !== null && APP_ORIGINS.includes(origin);
        if (fromApp && request.method === "OPTIONS") {
          return new Response(null, { status: 204, headers: corsHeaders(origin) });
        }
        const result = await next();
        if (fromApp) {
          for (const [k, v] of Object.entries(corsHeaders(origin))) result.response.headers.set(k, v);
        }
        return result;
      });
      
      const csrf = createCsrfMiddleware({
        filter: (ctx) => ctx.handlerType === "serverFn",
        secFetchSite: (site, ctx) =>
          site === "same-origin" || APP_ORIGINS.includes(ctx.request.headers.get("Origin") ?? ""),
        origin: (origin, ctx) => origin === new URL(ctx.request.url).origin || APP_ORIGINS.includes(origin),
      });
      
      export const startInstance = createStart(() => ({
        requestMiddleware: [appCors, csrf],
      }));
      Lovable's own src/start.ts already has an error handler and a CSRF middleware: keep the error handler, and use these two in place of its CSRF middleware. Or ask Lovable to do it:
      In src/start.ts, let my mobile app call server functions. Accept requests from the origins https://localhost and capacitor://localhost with CORS: answer OPTIONS requests, allow the content-type, accept, authorization and x-tsr-serverfn headers, and expose x-tss-serialized, x-tss-raw and x-tss-context. Make the CSRF middleware accept those two origins for server function calls. Keep the rest of the file as it is.
      Then publish again.
    4. Build. Before compiling, Corenna calls your website the way the app will. If the site doesn't answer, redirects, or turns the app away, the build stops there and says what to change, so a broken app never gets built.

    Signed-in calls from the app carry the user's token in a header (see below), so this opens your server functions to the app without loosening the website's own protection.

    What stops a build, and why

    Corenna checks for these before anything is compiled, and names the file it found:

    • Server functions with no hosted backend. Set the Hosted backend URL as above.
    • Calls to your own server by a relative URL, like fetch("/api/..."). In the app, a relative URL points at the phone, not your website. Use the site's full https:// address, or move the call into a server function.
    • Server code that reads a cookie session. The app calls your website from its own origin, where the site's cookies are never sent, so the server would see a signed-out user. Send the user's access token in an Authorization header instead. With Supabase, that is the session's access_token, checked on the server with auth.getUser(token).
    • A server.url in capacitor.config. It makes the app load a website instead of the app Corenna builds. It is for live reload during development; remove it.
    • Old versions. The mobile build needs @tanstack/react-start 1.121.0 or later, and sending server functions to a hosted backend needs @tanstack/start-client-core 1.153.0 or later. New Lovable projects are well past both.

    Server routes, such as API endpoints for webhooks, are fine: they stay on your website, and the build just notes that the app must reach them by the site's full address.

    Build it

    1. Connect the Lovable project to GitHub: add a GitHub connection under Workspace settings → Git → GitHub, then click Connect in the project's GitHub settings.
    2. Sign in to Corenna with GitHub, give the Corenna GitHub App access to the repo, and create the project. Set the Hosted backend URL if the app needs one.
    3. Start an Android build: an .apk to install on a phone, or an .aab for Google Play. Your first successful Android build is free, with no card.
    4. For iPhone, you need an Apple Developer account ($99/year, paid to Apple) and its certificate and provisioning profile. The build runs on GitHub's macOS runners, so no Mac is needed.

    Good to know

    • The app's pages, scripts and images are inside the app, so it opens without a connection; anything that needs a server or another website needs one. That includes fonts loaded from Google Fonts, which fall back to the phone's own fonts when offline.
    • To update the app: change it in Lovable, publish if a server function changed, and build again. The build number goes up after every successful build, so each new version installs over the last.
    Keep reading