The morning after I put a wallpaper app update on a staged rollout, I sat looking at the track list in Play Console. Internal, Closed, Open, Production — for me it is just the order I walk through every time.
The same screen means something else to someone who opened their account recently. The Production page is sitting right there, and it cannot be clicked.
Let me say this up front. My own account predates November 13, 2023, so this requirement does not apply to it. I cannot tell you a story about the struggle of recruiting twelve testers. What I can write about is the counting and the record-keeping, seen from someone who runs the same closed track for staged rollouts on every release.
The greyed-out button has nothing to do with how good your app is
Google Play requires personal developer accounts created after November 13, 2023 to test before an app is eligible for distribution. You need a closed test with at least 12 testers who have been opted in continuously for at least 14 days, and then you apply for production access from the Dashboard in Play Console.
Until you meet that, the Production and Pre-registration pages stay disabled. Organization accounts, and personal accounts created before November 13, 2023, are exempt. And the requirement applies per app. The wording lives in App testing requirements for new personal developer accounts (Play Console Help).
For a long stretch I read every store-side blocker as a problem with the app itself. That habit cost me evenings. When the build uploads fine and the button is still grey, the thing to fix is not in the code.
Getting through review and unlocking the right to distribute are two separate tracks of work. They sit on the same screen, which makes them easy to conflate, and rushing one does not move the other.
What is counted is not headcount, it is the unbroken stretch of days
This is where most people misread the rule. It is not "how many people have joined at some point." It is how many testers were opted in for the whole 14 days immediately before you apply.
The official FAQ is direct about it: someone who opts in, stays for fewer than 14 days, and opts out does not count, and if a tester opts out and comes back, the 14 days have to be consecutive to count. So a departure is not "one fewer tester." It resets that person's clock to zero.
Which means running with exactly twelve is thin ice. One person turns off notifications, deletes the app, and drops out of the test — and your application date slides two weeks. I would invite more than twelve, so the number that remains after a couple of departures is still enough.
There is one more thing. After you apply, Google may ask you to keep testing if fewer than twelve testers were opted in, or if tester engagement during the period was insufficient. Getting the shape of the number right does not carry you past that part.
Open the tracks in order, or the 14 days never start
Each track has a different job, and they unlock in a fixed order.
| Track | What it is for | What it takes to use |
|---|---|---|
| Internal testing | Push a build to a handful of trusted people, right away | Nothing. Works before app setup is complete |
| Closed testing | Share with a group you choose. This is where the 14 days are counted | Completed app setup |
| Open testing | A public test anyone can join | Production access already granted |
| Production | Available to everyone on Google Play | Meet the closed-test criteria, apply, and be approved |
If you were planning to open the Open testing track first and recruit from there, that page will not open until production access exists. The order is Internal, then Closed, then the application, then Production.
In practice this sequence keeps things calm:
- Ship to Internal testing and confirm launch plus the main screens on your own device and a friend's
- Complete app setup — store listing, Data safety, content rating
- Create the Closed testing track and hand out the tester list or the opt-in link
- Write down the join dates and start counting the 14 days from there
- Reply to the feedback that arrives, fix things, and push the fixes back to the closed track
- Apply for production access from the Dashboard
If you are shipping a project exported from Rork through EAS, you can stop track mix-ups at the config level.
{
"submit": {
"internal": {
"android": {
"track": "internal"
}
},
"closed": {
"android": {
"track": "alpha"
}
}
}
}# Straight to the people close to you (works before app setup is done)
eas build -p android --profile production
eas submit -p android --profile internal --latest
# Into the track where the 14 days are counted (after app setup)
eas submit -p android --profile closed --latestThe values track accepts are internal, alpha, beta and production. If your closed test lives on a custom-named track, upload to alpha and promote the release to that track inside Play Console. Either way, open the Console afterwards and look at which track the build actually landed in. A version-number collision will stall you in a different place, so the number in "Version code 1 has already been used" belongs to either app.json or EAS is worth reading first.
The application form asks what you did during those 14 days
You start the application from "Apply for production" on the Dashboard. The form has three sections, and the questions are knowable in advance.
| Section | What it asks |
|---|---|
| About your closed test | How easy recruiting testers was / whether testers used all the features / whether their usage matched what you expect from production users, and any differences you saw / a summary of the feedback and how you collected it |
| About your app/game | Your target audience, as specifically as you can put it / the value your app gives users / your estimated install range for the first year |
| About your production readiness | What you changed based on what the closed test taught you / how you decided it was ready |
Reading that list first changes how you spend the two weeks. Whether testers touched every feature, what they said, what you changed in response — none of that can be reconstructed from memory afterwards.
The 14 days are not a waiting period. They are when you write the answers. Of everything I would prepare, this reframing is the one that pays.
Two small mechanics. Each section only saves when you press "Next", so a half-written page disappears if you navigate away. And if your app needs a login, leave working test credentials in the Console. Review usually comes back within seven days.
A closed test shows you real device behaviour and stops there; the store build is a different animal. I wrote about closing that gap in I don't call a screen finished just because it ran in Rork Companion.
One file and one script are enough for those two weeks
The records do not need to be elaborate. Join date and drop-out date per tester, a one-line summary of each piece of feedback with where it came from, and what you changed with the version you pushed it in. With those three, most of the form fills itself.
Counting join dates by hand goes wrong, so I hand that to a small script.
"""Count how many testers have been opted in continuously for 14+ days.
Usage: python3 closed_test_status.py testers.csv --as-of 2026-09-09
CSV columns: tester,opted_in,opted_out (empty opted_out means still in)
"""
import argparse, csv, sys
from datetime import date, timedelta
REQUIRED_DAYS = 14
REQUIRED_TESTERS = 12
def parse_day(value):
if not value or not value.strip():
return None
return date.fromisoformat(value.strip())
def main():
ap = argparse.ArgumentParser()
ap.add_argument("csv_path")
ap.add_argument("--as-of", default=date.today().isoformat())
args = ap.parse_args()
as_of = parse_day(args.as_of)
active, dropped = [], []
with open(args.csv_path, newline="", encoding="utf-8") as fh:
for row in csv.DictReader(fh):
joined, left = parse_day(row["opted_in"]), parse_day(row["opted_out"])
if joined is None or joined > as_of:
continue # has not joined yet
if left is not None and left <= as_of:
dropped.append((row["tester"], (left - joined).days))
continue # a departure resets the clock
active.append((row["tester"], (as_of - joined).days,
joined + timedelta(days=REQUIRED_DAYS)))
qualified = [t for t in active if t[1] >= REQUIRED_DAYS]
print(f"as of: {as_of} opted in: {len(active)} dropped: {len(dropped)}")
print(f"{REQUIRED_DAYS}+ continuous days: {len(qualified)} / {REQUIRED_TESTERS} needed")
for name, days, _ in sorted(active, key=lambda t: t[1], reverse=True):
mark = "OK " if days >= REQUIRED_DAYS else " "
print(f" {mark}{name:<12} {days:>3} days")
for name, days in dropped:
print(f" OUT {name:<12} left after {days} days (rejoining restarts the 14)")
if len(qualified) >= REQUIRED_TESTERS:
print("-> criteria for applying for production access are met")
return 0
eligible_dates = sorted(t[2] for t in active)
if len(eligible_dates) < REQUIRED_TESTERS:
need = REQUIRED_TESTERS - len(eligible_dates)
print(f"-> {len(active)} opted in. invite {need} more to reach the minimum")
return 1
print(f"-> if nobody leaves, criteria are met on {eligible_dates[REQUIRED_TESTERS - 1]}")
return 1
if __name__ == "__main__":
sys.exit(main())Run it against thirteen invitations where one person left after nine days, and it answers like this.
as of: 2026-09-09 opted in: 12 dropped: 1
14+ continuous days: 7 / 12 needed
OK t01 20 days
OK t02 20 days
OK t03 19 days
OK t04 18 days
OK t05 18 days
OK t06 16 days
OK t07 15 days
t08 12 days
t09 8 days
t10 7 days
t11 6 days
t12 6 days
OUT t13 left after 9 days (rejoining restarts the 14)
-> if nobody leaves, criteria are met on 2026-09-17That last line is what you plan the application date around. Looking only at the headcount, you would read "twelve are in, so I can apply." Looking at the days, you can see it is not there yet.
Deadlines on the store side rarely arrive one at a time, either. If you have an update coming, put dates like the ones in August 31 for target API 36 means different things depending on whether you ship an update on the same calendar.
What to do today
Before you send the invitations, make one note file. Three headings, taken straight from the form: about your closed test, about your app, why it is ready for production. Leave the bodies empty.
Fourteen days from now, whether you can fill those blanks decides between applying once and waiting another fortnight. As an indie developer, I have no way to buy back a lost fortnight — so I keep making the place to write before there is anything to write in it.
Thank you for reading. I hope the next step is a shorter one for you than it looks from in front of that greyed-out button.