DEV Community

Cover image for The eject button is a myth: what owning an AI-built app really takes
Dave Kurian
Dave Kurian

Posted on Originally published at otf-kit.dev

The eject button is a myth: what owning an AI-built app really takes

AI can hand you a working prototype in a weekend. Screens render, buttons respond, data appears. It feels like the app is ninety percent done and the last step is pressing an eject button that drops you into clean, owned production code.

That button does not exist. There is no single export that converts generated output into an app you can run, update, and answer for. What exists instead is a slower handoff: six systems you need to read, name, and take responsibility for before the app is really yours. Teams that plan for that handoff ship. Teams that wait for the button rewrite.

The confusion is understandable because the old vocabulary still floats around. Expo used to talk about managed versus bare workflows and ejecting from one to the other. That framing is now deprecated in favor of continuous native generation, where native projects are generated from configuration on demand. The direction of the whole ecosystem is away from one-time exports and toward repeatable generation from a config you own. If you take one idea from this post, take that one: ownership is the config, not the output.

Why the demo feels done before the app is shippable

A demo proves one path through happy-path data on one device with one set of credentials. Production is every other path: expired sessions, revoked tokens, slow networks, denied permissions, OS updates, store review, and the user who does everything in the wrong order. Generated code usually covers the demo path well and leaves the rest as TODO comments or silent assumptions.

The gap shows up in predictable places. Environment variables are hardcoded or pasted into chat. Auth works until the refresh token expires. Uploads work on Wi-Fi and fail on a train. Push notifications work for the developer device and nobody else. Each gap is small on its own. Together they are the difference between a prototype and a product.

A useful rule from the shipping checklist lane: if you cannot describe how the app behaves with no network, an expired session, and a denied permission, you do not yet own it. The post on shipping an AI MVP to production walks that exact gap analysis, and it pairs well with this one.

The six systems you actually own

Ownership breaks down into six systems. You do not need to have written each one. You need to be able to read it, change it safely, and roll it back when the change is wrong.

First, the repository contract. Can a new contributor, human or agent, open the repo and know where screens, data access, and platform config live? Generated projects often scatter logic across chat-created files with overlapping names. The fix is boring and high-use: one folder convention, one naming rule, one README that states how to run, test, and release. The guide to an agent-readable repository structure is the standard I recommend here, because the second reader of your code will often be an AI agent, and agents follow structure better than intent.

Second, configuration and secrets. Every key, URL, and feature flag in the app needs a named home that is not chat history. Production builds must pull secrets from build-time profiles or a secrets store, never from code pasted during generation. A clean test is to delete your local .env file and rebuild from documented steps only. If the build fails, your config lives in your head, not in the repo. The EAS secrets walkthrough in keeping build secrets out of bundles shows the split between local, preview, and production profiles that makes this repeatable.

Third, auth sessions across restarts. Sign-in screens are easy to generate. Session persistence across app kills, OS updates, and token rotation is where ownership is proven. You should be able to state where tokens are stored, how refresh is scheduled, and what the user sees when refresh fails. If those answers are vague, read how Supabase auth sessions stay signed in and port its storage and refresh checks to your own stack before you ship.

Fourth, data and uploads on real networks. Mobile users lose connectivity mid-action. An owned app queues mutations, retries uploads with progress, and resolves conflicts without data loss. The offline-first mutation queue pattern gives you the queue shape, and the storage upload checks in the Supabase lane cover resumable uploads with visible state. Port at least the queue before your first production release.

Fifth, releases and rollback. Owning an app means owning the bad day: a broken bundle reaches users and you need it gone in minutes, not hours. That requires staged rollouts, a known-good channel to roll back to, and a runbook the whole team can follow under stress. Write the runbook before you need it using the EAS update rollback plan as the template, then rehearse it once on a preview channel.

Sixth, store submission readiness. Review teams do not care how the app was built. They check privacy manifests, permission strings, account deletion, crash-free launches, and accurate screenshots. The app store submission checklist is the closest thing to a pre-flight inspection this site publishes. Run it a week before you plan to submit, not the night before.

Notice what is missing from this list: a step called eject. Each system is adopted one at a time, verified by a behavior you can observe, not by a ceremony you perform once.

What to do in the first week of ownership

Start with a reading pass, not a rewrite. Give yourself two days to read every file the generator created and annotate three things per file: what it does, what it depends on, and what breaks if it is deleted. Files you cannot annotate are your risk register. Most teams find between five and fifteen such files, usually around auth callbacks, deep linking, push registration, and native config.

Next, freeze the config surface. Move every secret and URL into documented profiles, wire the three build flavors, and prove a clean-machine build works. This one change removes an entire class of it-works-on-my-machine failures:

# Prove the build is reproducible from docs alone
rm -rf .env
eas build --profile preview --platform all --non-interactive
# Expected: green build with no manual key pasting
# If it prompts for a secret, that secret still lives in your head
Enter fullscreen mode Exit fullscreen mode

Then stabilize the session boundary, because auth touches everything and fails in ways users notice first. A minimal refresh guard you can read in one sitting looks like this:

// session-refresh.ts — refresh before expiry, fail loudly when refresh fails
import { AppState } from "react-native";

const REFRESH_MARGIN_MS = 5 * 60 * 1000;

export function watchSession(getSession: () => Promise<{ expiresAt: number } | null>) {
  const check = async () => {
    const session = await getSession();
    if (!session) return redirectToSignIn("no-session");
    if (session.expiresAt - Date.now() < REFRESH_MARGIN_MS) {
      const renewed = await tryRefresh();
      if (!renewed) return redirectToSignIn("refresh-failed");
    }
  };
  const sub = AppState.addEventListener("change", (state) => {
    if (state === "active") void check();
  });
  return () => sub.remove();
}
Enter fullscreen mode Exit fullscreen mode

The point of this snippet is not the implementation, which you will adapt, but the contract it states in plain code: sessions are checked on foreground, refreshed with margin, and failures route to sign-in instead of hanging. If your generated auth code cannot express that contract, replace it with code that can.

The rewrite trap and how to avoid it

The most common failure after the eject myth dies is the full rewrite. The team decides the generated code is untrustworthy, throws it away, and starts clean. Six weeks later they have a cleaner repo with fewer features and the same six systems still unowned, because the rewrite rebuilt screens instead of adopting config, sessions, queues, releases, and review readiness.

A cheaper sequence works better. Keep the generated UI that users already validated. Replace one system per week in a fixed order: config, sessions, data queue, releases, then submission readiness, with the repo contract tightening throughout. Each step leaves the app shippable, so progress compounds instead of resetting. If a step cannot be completed without breaking the app for a week, the step is too big. Slice it until the app stays releasable every Friday.

This is also where starter-kit choice pays off retroactively. If you had picked the kit with the six checks in choosing a starter kit without a later rewrite, several of these systems would already have homes. If you did not, impose those homes now. The cost of imposing structure late is real but far lower than the cost of a second rewrite.

A short contract for AI-built apps

Before calling any AI-built app production-ready, write down these six answers and keep them next to the README. What builds each flavor and where do its secrets live. Where sessions persist and what happens on refresh failure. How offline actions queue and retry. Which channel serves production and how rollback runs. What the submission checklist flagged and when it was last run. Who can cut a release when the primary developer is offline.

None of these answers require a button. All of them require reading. That is the actual handoff from generated prototype to owned product: not an export step, but a set of behaviors you can describe, test, and repeat. Teams that do this work find that AI output was a genuine head start. Teams that skip it learn the same lesson later, usually during an outage, a rejected review, or a lost user session that nobody can explain.

Skip the search for the eject button. Adopt the six systems, verify each with an observable behavior, and ship the app you can actually stand behind.

Sources

Top comments (0)