Build a mobile app's backend

Prompt:

Build the backend for our iPhone and Android app on Turn Zero Cloud, with sign-in.

Also works: "Our Expo app needs a backend and user accounts." "Host the API our mobile app calls." "The app has no website, only the app." "Our Mac and Windows app needs sign-in."

Turn Zero Cloud hosts the backend, runs the sign-in, and applies the checks in front of it. The mobile app itself is yours, built and signed with its own tools and shipped through the stores.

What your tool does

  • Reads the mobile-backend skill with read_context, id skill:mobile-backend, or as the MCP prompt of that name. The skill lists the steps below in order.
  • Creates the application with create_application, and submits a manifest declaring {"kind": "accounts"} with submit_manifest, so the application has a realm, its list of end users. A new application has one environment, production, and one realm, production's.
  • Builds and deploys the backend with no web client, so the artifact contains no client/ folder.
  • Declares the app on the production realm with configure_realm, the debug build and the store build both, and confirms it with read_realm.
  • Writes the app's sign-in against the production hostname, the bearer on every backend call, and the version header beside it.
  • Where the app receives notifications, declares {"kind": "push"}, configures each provider with configure_push, registers each device after sign-in, and sends from the backend through the Push package.
  • Before each release, reads the versions in use with read_realm, and only then deploys to production.
  • Where you turned on a development environment, declares the debug build on the development realm instead, points it at the development hostname, and releases with promote.

What you need

  • The app's identifier in reverse-domain form, such as com.example.app.
  • A development build of the app that you can run in a simulator, an emulator, or on a phone.

Before your AI starts

This section is for your AI tool: what it checks and gathers before it begins. You don't need to do these steps yourself.

  • A connected, signed-in tool (Connect your tool).
  • The sign-in methods the app offers, such as the emailed code, Google, Sign in with Apple, or passkeys (Add sign-in to your app).
  • The app's redirect scheme, and its app signing identities (the iOS bundle identity or the Android signing identity) where passkeys or https redirects are wanted.

Steps

1. Create the application with no web client

A web client is optional. The backend is ordinary Node.js code exposing one server that honors PORT, as Deploy an application describes. An application without a web client ships no client/ folder, and every request reaches the backend's own routes.

Create the application with create_application, choosing its plan. A new application has one environment, production, so the debug build and the store build both call <label>.ai.host. Once you turn development on with create_environment, the debug build calls <label>-dev.ai.host instead.

2. Declare the accounts service

The manifest's services list declares the accounts service. Submit it with submit_manifest, and each environment the application has gains its own realm of users: production's alone on one environment.

{
  "manifest_version": 1,
  "services": [{ "kind": "accounts" }],
  "health": "/health",
  "region": "usa",
  "egress": [],
  "audience": { "kind": "public" },
  "packages": []
}

Enable the sign-in methods on each realm with configure_realm. The emailed code and passkeys work inside the app's browser sheet as they do on the web. For work accounts, step 5 of Add sign-in to your app gives the redirect URI to register, and step 6 the workforce audience.

3. Declare the app on the realm

The app is a client of the realm, known by its client_id and its redirect URIs. On one environment, declare the debug build on the production realm, which configure_realm reaches where the call names no environment.

{
  "application": "<application id>",
  "clients": [
    { "client_id": "com.example.app", "redirect_uris": ["com.example.app:/callback"] }
  ]
}

This debug build has a custom-scheme redirect, as a desktop app's loopback redirect would be, and no app signing identity, the iOS bundle or Android signing identity that passkeys and app links read. It signs in through the browser redirect alone, with no passkeys and no exchange of an Apple or Google sign-in token. To test those, declare the debug build's signing identity on the production realm. The platform then publishes that identity beside the store build's.

read_realm returns the declared clients. Declare the store build's redirect URIs and identities on the production realm too, before it ships. With development turned on, declare the debug build on the development realm instead, with environment: "development", and the store build on production's. Sign in from a native app covers the app signing identities and the three redirect forms.

4. Sign in from a development build

On one environment, a debug build and a store build both use the production hostname and sign in on production's realm. With development turned on, a debug build uses the development hostname, and a store build the production one. The sign-in runs in the system browser sheet, with the authorization code flow and Proof Key for Code Exchange (PKCE). Expo Go's exp:// redirect is outside the redirect rule, so test in a development build with your own scheme.

Keep the flow in one module the app copies into its code. It takes three dependencies as parameters: opening the sign-in URL and receiving the redirect, storing and reading a secret, and fetching. It imports nothing, so a test can run it with stubs.

The module contains the authorization request, the code exchange, the secure store for the pair, the refresh with at most one refresh running at a time, and the bearer fetch. The native sign-in guide describes each request and its response.

Where the app signs in with Apple's or Google's own software development kit (SDK) instead, it exchanges the provider's ID token at the realm's token endpoint for the same session. Step 10 of the native sign-in guide covers that exchange.

5. Reach the backend from a simulator or an emulator

Point the debug build at the production hostname, or at the development hostname once development is turned on. The iOS Simulator and the Android Emulator both reach it over your computer's internet connection, as a phone does.

On one environment, the accounts your testers sign in with are end users of the live application, on the same list as your real users. Turning development on gives testers a separate list.

A backend running on your own machine offers no sign-in. The realm, its endpoints, and the bearer check run on the platform alone. For other local work, the iOS Simulator reaches your machine at localhost, and the Android Emulator at 10.0.2.2.

6. Call the backend with the bearer

The app sends the session token as a bearer on every backend call. The platform checks it before the request reaches your code, and refuses an expired or ended one with 401. No test checks this sample.

Authorization: Bearer turnzero_cloud_usr_1....

The backend verifies the token through the account package's verify client, as it would verify a browser's cookie. Verifying an end user explains the client and the header the platform sets beside it.

7. Send the version header

The app states its client_id and its version on every call. When the realm's minimum rises above that version, the platform refuses the call with 426 and a store link. No test checks this sample.

x-turnzero-cloud-client: com.example.app/1.0.0

Show a screen that opens the store link, and stop retrying. Keep installed clients compatible covers the minimum and its warnings.

8. Send push notifications

Where the app receives notifications, add {"kind": "push"} to the manifest's services. Store the Apple signing key and the Google service-account file with store_secret, naming each file as value_file, as Store a secret describes. Then name them in configure_push for each environment the application has.

After sign-in, the app registers its device under the bearer with a PUT to /__account/devices/<installation id>. A registration ends when the session it was made under is revoked, by a sign-out among other endings, or when the user is revoked. A session that merely expires leaves its registration in place. The app registers again after each sign-in, which moves the registration to the new session. The backend sends through the Push package's client under the platform credential, listing the users to reach. Send push notifications covers each step and what a push may contain.

9. Read the versions in use before a release

A deploy to production changes the backend every installed copy of the app calls, and so does a promote where development is turned on. First make sure the store build is declared on the production realm with configure_realm. Then read the versions in use on that realm with read_realm.

Where the new backend drops something an installed version still calls, keep the backend compatible or raise the minimum first. Then deploy, or promote with promote where development is turned on.

Expected result

The application runs with no web client, and its realm, production's on one environment, has the app declared on it. In a development build, the sign-in opens in the browser sheet and returns to the app with a session. The backend responds to the app's calls under the bearer, and each response gives the serving version in x-turnzero-cloud-version.

Refusals

Refusal Status Cause Remedy
invalid_client 400 The client_id matches no client the environment's realm declares. Declare the app with configure_realm on that environment's realm.
invalid_redirect_uri 400 The redirect URI is not declared, or is outside the three forms, such as Expo Go's exp://. Use a development build with your own scheme, and declare its URI.
authentication_required 401 The bearer is expired, ended, or another realm's. Refresh at the token endpoint, and sign in again where the refresh is refused.
client_upgrade_required 426 The app's version is below its client's minimum on this realm. Update the app from the store link, or lower the minimum.