import { Callout } from '@/components/ui/callout';
The thing I got wrong on my first Rork trial was not the message counter. It was that I waited until the credits were gone before trying to move the project out of Rork.
Inside the browser, the prototype ran. On my own machine, it stopped before the build even started. What was missing was not code. It was the identifiers that sit around the code.
So the useful question is not "how many screens did I get for free." It is whether anything survived outside Rork. Answer that, and the upgrade decision answers itself.
What Ends the Trial Is Messages, Not Days
Asking "how many days is the free trial" hides the actual constraint, because days are not what stops you.
| Common assumption | What actually binds |
|---|---|
| A clock runs out | The message quota runs out. Untouched, an account can sit there for weeks |
| Bad generations are free | A round trip costs a message even when the output misses. Vague prompts drain fastest |
| Upgrading adds leftovers to the new plan | Plans are monthly. Switching starts a fresh allowance |
| Running on a real device requires paying | Companion testing is available inside the free tier |
Because the unit is one round trip, consumption tracks how often you have to ask again rather than how big the app is. For me, a single-screen app has landed somewhere around 30–80 messages, and something with three or four interacting features around 150–300. Rework, not ambition, is what doubles the number.
Plan names and prices get revised. When money is the deciding factor, read the figure off the Rork pricing page on the day rather than from any article's table. For how the tiers differ in intent, see Rork Pricing Compared: Free vs Pro vs Max.
Measure the Trial by What Runs Outside Rork
Rork has an Export function that pushes the whole React Native + Expo project to GitHub. Running it once while you are still on the free tier is what keeps the trial from evaporating.
An exported project is not automatically a buildable one, though. Settings that Rork did not need while it hosted the preview become mandatory the moment the project is somewhere else — and you normally discover them after npm install, which is the slowest possible moment to find out.
This is the script I run before installing anything. Node built-ins only.
#!/usr/bin/env node
// audit-export.mjs — decide whether an Expo project exported from Rork
// can build outside Rork, before running npm install.
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
import { join, extname } from "node:path";
const root = process.argv[2] ?? ".";
const read = (p) => JSON.parse(readFileSync(join(root, p), "utf8"));
const problems = [];
const flag = (level, msg, why) => problems.push({ level, msg, why });
// 1) app.json (app.config.js needs evaluation and is out of scope here)
if (!existsSync(join(root, "app.json"))) {
flag("FAIL", "no app.json", "for app.config.js, run expo config --json first");
} else {
const { expo = {} } = read("app.json");
if (!expo.ios?.bundleIdentifier) flag("FAIL", "ios.bundleIdentifier missing", "EAS build refuses to start");
if (!expo.android?.package) flag("FAIL", "android.package missing", "same, on the Android side");
if (!expo.extra?.eas?.projectId) flag("FAIL", "extra.eas.projectId missing", "eas build / submit cannot resolve the project");
if (!expo.scheme) flag("WARN", "scheme missing", "deep links and auth callbacks will not come back");
}
// 2) reconcile EXPO_PUBLIC_* references against .env
const walk = (dir) =>
readdirSync(dir).flatMap((name) => {
if (name === "node_modules" || name.startsWith(".")) return [];
const p = join(dir, name);
return statSync(p).isDirectory()
? walk(p)
: [".ts", ".tsx", ".js", ".jsx"].includes(extname(name)) ? [p] : [];
});
const envFile = existsSync(join(root, ".env")) ? readFileSync(join(root, ".env"), "utf8") : "";
const defined = new Set([...envFile.matchAll(/^([A-Z0-9_]+)=/gm)].map((m) => m[1]));
const used = new Map();
for (const file of walk(root)) {
const src = readFileSync(file, "utf8");
for (const m of src.matchAll(/process\.env\.(EXPO_PUBLIC_[A-Z0-9_]+)/g)) {
if (!used.has(m[1])) used.set(m[1], file);
}
}
for (const [name, file] of used) {
if (!defined.has(name)) flag("FAIL", `${name} is not defined in .env`, `referenced from ${file}`);
}
// 3) report
const fails = problems.filter((p) => p.level === "FAIL").length;
console.log(`export audit: ${root} (${used.size} EXPO_PUBLIC_* references)`);
for (const p of problems) console.log(`[${p.level}] ${p.msg}\n -> ${p.why}`);
console.log(problems.length === 0 ? "clean" : `blockers ${fails} / warnings ${problems.length - fails}`);
process.exit(fails > 0 ? 1 : 0);Run against a freshly exported project — two screens, two environment variables — it reports:
export audit: . (2 EXPO_PUBLIC_* references)
[FAIL] ios.bundleIdentifier missing
-> EAS build refuses to start
[FAIL] android.package missing
-> same, on the Android side
[FAIL] extra.eas.projectId missing
-> eas build / submit cannot resolve the project
[WARN] scheme missing
-> deep links and auth callbacks will not come back
[FAIL] EXPO_PUBLIC_ANALYTICS_KEY is not defined in .env
-> referenced from app/index.tsx
blockers 4 / warnings 1Add the three keys to app.json, add the missing line to .env, run it again:
export audit: . (2 EXPO_PUBLIC_* references)
cleanIt signals through the exit code, so it drops into CI unchanged. If it returns clean on the last day of your trial, the trial produced an asset rather than a memory.
What Each Finding Actually Costs You
All five are things Rork did not need while it was hosting your project. Knowing what they do makes the findings easy to clear.
| Field | Why it becomes mandatory outside Rork | Changeable later? |
|---|---|---|
| ios.bundleIdentifier | It is your app's identity in App Store Connect. EAS will not start without it | Effectively no, once published |
| android.package | The same identity on Google Play. Easier to manage if it matches iOS | No, once published |
| extra.eas.projectId | The destination for builds and OTA updates. Local runs work without it; cloud builds do not | Yes |
| scheme | Where deep links and auth callbacks return to | Yes, but existing links break |
| EXPO_PUBLIC_* | Values you set inside Rork are not always part of the export | Yes |
The right-hand column is the point. The first two are one-way doors, and filling them with a placeholder "for now" is exactly how a throwaway identifier ends up shipped and frozen. If you are still deciding on names, read The names you can change, and the ones you can't before your first release.
Note the prefix on the last row: EXPO_PUBLIC_ variables are embedded in the client bundle. Never put a secret there. Keep placeholders like YOUR_API_KEY in .env and move real credentials to a server or a build-time secret.
Four Ways the Free Tier Drains
Prompts that are vague enough to need a second pass
Quota goes to retries, not to features. Spending the first message on screen layout, data shape, and which APIs are involved is cheaper than three corrective rounds afterwards.
Asking for a large feature in one shot
Big requests are hard to partially fix. Finishing one feature at a time and checking each on a device keeps consumption predictable. For how long free device testing holds up, see How far Rork Companion's free device testing goes.
Hand edits disappearing on the next generation
Once you have exported and started editing by hand, running another generation in Rork can revert those edits. Keeping Manual Fixes Alive Across Rork Regenerations covers where to draw the boundary — worth internalizing while it is still free to get wrong.
Signing up "to have it ready"
Free credits expire. I have let a batch lapse without touching them. Create the account on a day you have actually blocked out time to build.
Where Paying Starts to Make Sense
By the time the free tier ends, you already hold the evidence.
| How the trial ended | What to do next |
|---|---|
Audit is clean and the app feels right on a device | Upgrade and take it to release. Budget 200–400 messages for a launch including review round trips |
| Audit is clean but the idea is still fuzzy | Do not upgrade yet. Undefined scope burns quota faster than anything else |
| UI comes out well but a required capability does not close inside Rork | Compare continuing in the exported project with Expo CLI as a serious option |
| Quota is left over because you had no time | Do not upgrade. Time is the constraint, not messages |
| A second and third project are already planned | A tier that spans projects lowers the per-project cost sharply |
What changed things for me was treating the free tier as a place to test fit rather than a place to build. Building can wait until after you pay. Testing fit cannot.
Your Next Step
If you have not signed up, create an account at Rork, generate a single screen, and push it through Export the same day. Then run the script above — the blockers count is your first real result. When it reaches zero, you have everything you need to decide about paying.
The free tier is not there to be used up. It is there to make the decision arrive sooner.