App Signing & Production Builds
Build a signed, distributable release of your app in the cloud — the kind you submit to the App Store or Play Store, or hand to a tester outside VibeView — from the same pipeline you already use for cloud builds.
A production build is not something you run in VibeView. Cloud builds
normally produce a simulator or emulator build that installs onto a cloud
device for testing. A production build is different: it produces a signed
release artifact (an .ipa or .aab/.apk) meant for a real device or a
store, so VibeView cannot install or run it. It does not appear in an app’s
Build History, and you can’t start a session against it. Instead it shows up
on the Builds page with a Download action — you build it here, then
take the file wherever it needs to go next. For the App Store and Play
Store, vibeview submit can take it there for you — see Store
Submission.
Production builds are available for iOS, tvOS, Android, and Android TV.
Before you start: signing credentials
A production build needs a signing credential first. There are two ways to get one into VibeView:
- Upload existing material — an Apple certificate and profile you exported yourself, or an Android keystore you already have.
- Generate it — VibeView creates an Android keystore for you, or, once you’ve connected your Apple account (see below), creates the iOS/tvOS certificate and provisioning profile through Apple’s API.
iOS needs two files:
- a distribution certificate (
.p12) - a provisioning profile (
.mobileprovision) that matches it
Android needs a keystore (.jks), plus its key alias and both the
keystore and key passwords.
Android TV needs no credential of its own: an Android TV production build signs with your Android credential — one keystore covers both your phone and TV builds, so there is nothing extra to upload or rotate.
tvOS uses the same Apple Distribution certificate as iOS, but
provisioning profiles are platform-specific — a tvOS profile is not the
same file as an iOS one. So a tvOS credential is uploaded separately: the
same .p12, paired with your tvOS provisioning profile.
Upload from the CLI:
vibeview credentials add --platform ios --file cert.p12 --profile app.mobileprovision [--app <app-id>]
vibeview credentials add --platform tvos --file cert.p12 --profile app-tvos.mobileprovision [--app <app-id>]
vibeview credentials add --platform android --file release.jks [--app <app-id>]
vibeview credentials list [--app <app-id>]
vibeview credentials rm <name> [--app <app-id>]
Every password is prompted for interactively with hidden input — there is no
flag for passwords, so one never ends up in your shell history. Each
password (keystore password, key password, .p12 password) must be at
least 8 characters — shorter passwords are rejected at upload. The key
alias is not a password and has no length requirement.
Don’t have a keystore? Generate one
If you’ve never created an Android keystore, VibeView can generate one for
you — no Java or keytool needed on your machine:
vibeview credentials generate --platform android --name <name>
This creates a new keystore, stores it as a signing credential (so
production builds pick it up immediately), saves the keystore file locally,
and prints its passwords. It’s still worth keeping your own backup of both,
but a generated credential is not gone if you lose them: an organization
admin can re-download the file and passwords later with
vibeview credentials download (see “Credentials you generated” below).
The same generated keystore signs both your Android and Android TV
production builds.
Re-running the command with a name that already exists refuses to replace
the stored keystore unless you pass --force. Be careful with --force:
replacing a keystore already used for a published app breaks its Play Store
updates — Google Play only accepts updates signed with the original key.
Connect your Apple account
To have VibeView generate iOS and tvOS credentials for you, store an App Store Connect API key first. An API key lets VibeView talk to Apple’s developer API on your team’s behalf — creating certificates and provisioning profiles — without ever touching your Apple ID password, and without any two-factor prompts.
Create one in App Store Connect under
Users and Access → Integrations: generate a team API key with the
App Manager or Admin role, note its Key ID and your Issuer
ID, and download the .p8 key file (Apple lets you download it once).
Then store it:
vibeview credentials asc-key add --key-id <KEY_ID> --issuer-id <ISSUER_ID> --file AuthKey_<KEY_ID>.p8 [--name team]
vibeview credentials asc-key list
vibeview credentials asc-key rm <name>
The key belongs to your whole organization (it’s an Apple team key, not
an app-level one). Most organizations need exactly one; if you store
several, pick one at generation time with --asc-key <name>. Adding a key
under an existing name replaces it — that’s how you rotate one. The .p8
file itself is an uploaded secret: asc-key list shows only the name, key
ID, and issuer ID, and the key can never be viewed or downloaded again.
Storing or removing a key requires the Admin role.
Generate iOS and tvOS credentials
With an App Store Connect key stored, one command replaces the whole
manual export-a-.p12-and-download-a-profile dance:
vibeview credentials generate --platform ios --name <name> --app <app-id>
vibeview credentials generate --platform tvos --name <name> --app <app-id>
VibeView creates an Apple Distribution certificate for your team (or reuses one it created earlier — see below), registers the app’s bundle id with Apple if it isn’t registered yet, creates an App Store provisioning profile for it, and stores the result as an ordinary signing credential — production builds pick it up immediately, exactly as if you had uploaded the same material yourself.
The bundle id comes from the app you pass with --app; pass
--bundle-id com.your.app instead (or as an override) if you want to
provision a different one. Because a provisioning profile is tied to its
bundle id, a generated Apple credential is scoped to the app when you use
--app. Re-generating under an existing name refuses to replace the
stored credential unless you pass --force, like every other credential.
Certificates are shared and capped. Apple limits each team to about
two distribution certificates, so VibeView is careful with them: when you
generate credentials for a second app (or for tvOS after iOS), it re-uses
the distribution certificate it already created for your team and only
issues a new profile. If Apple refuses to create a certificate because
your team is at the limit, the command fails with instructions: revoke an
unused certificate at developer.apple.com, or upload your own .p12 with
vibeview credentials add. VibeView never revokes certificates itself —
that stays your decision, in Apple’s portal.
Uploaded credentials are never readable back
Once uploaded, a credential’s file, passwords, and profile cannot be viewed
or downloaded again by anyone, including you. credentials list shows only
metadata: name, platform, scope, identity hash, and expiry. If you lose the
original file or forget a password, there’s nothing to recover — upload a
new credential (with a new name, or removing the old one first) instead.
The same rule covers your App Store Connect .p8 key.
Credentials you generated can be downloaded again
Credentials that VibeView generated — Android keystores from
credentials generate, and generated iOS/tvOS credentials — are the one
exception. An organization admin can re-download the files and
passwords:
vibeview credentials download <name> [--app <app-id>] [--out <dir>]
Every download is recorded in the organization’s audit log. This only applies to credentials generated from now on; anything you generated before this feature existed stays write-only, as promised at the time — and uploaded credentials stay write-only forever.
The identity hash in the IDENTITY column says which kind of value it is,
because they are not the same thing on every credential:
cert sha256:…— an Android keystore in PKCS#12 format (the default since JDK 9, and whatkeytool -genkeypairproduces today). This is the signing certificate’s own fingerprint, so it matcheskeytool -list -v -keystore release.jks.cert sha1:…— an iOS distribution certificate, matchingopenssl x509 -in cert.pem -noout -fingerprint -sha1.keystore-file sha256:…— a legacy JKS/JCEKS keystore, which VibeView cannot open. This is a hash of the keystore file, not of the certificate inside it, so it will not match whatkeytoolprints. Use it to tell two uploads apart, not to identify a certificate. These also show no expiry, since reading one means opening the keystore.
Rotating a credential
Re-uploading a credential under an existing name (in the same scope) replaces it — files, passwords, and profile all at once. That’s the intended way to rotate: keep the name, upload the new material, and every future production build picks it up. Finished builds are unaffected.
Managing credentials in the dashboard
Everything above can also be done on the Builds page, under the Signing tab. Any member of your organization can view it; adding and deleting credentials requires the Admin role.
The table shows each credential’s name, platform, scope, identity hash
(the same value credentials list prints), and expiry. When any credential
expires within 30 days, a warning banner names it, its expiry date, and
which production builds will start failing after that date.
Add credential opens an upload form: choose Apple distribution (then iOS or tvOS) or Android keystore, pick the scope (org-wide or a specific app), and provide the same files and passwords the CLI asks for. Passwords must be at least 8 characters, on a single line. Re-uploading under an existing name replaces it, exactly as from the CLI.
Delete asks for confirmation first. After a delete, new production builds stop using that credential — if nothing else valid exists in the same scope, they are refused until a replacement is uploaded. Finished builds are unaffected.
Credentials are just as write-only here as in the CLI: the dashboard shows metadata only, and an uploaded file or password can never be viewed or downloaded again — not by admins, not by support. The only exception is credentials VibeView generated, which admins can re-download (see “Credentials you generated can be downloaded again” above); every download is recorded in the audit log.
Scope
A credential is either organization-wide or tied to a specific app with
--app. Organization-wide credentials apply to every app’s production
builds of that platform; an app-scoped credential applies only to that app.
If both exist for the same app, the app-scoped one wins — even when the
app-scoped one has expired. A build never falls back from an app-scoped
credential to an organization-wide one: that would silently sign the app
with an identity you didn’t choose for it, so the build is refused instead
(see Expiry below).
App-scoped credentials only apply to builds that are linked to that
app. A production build run before the app exists (the build list shows
it under “No app yet”) can only use organization-wide credentials — create
the app with a regular simulator build first, set its appId in
vibeview.json (or pick it in the CLI’s prompt), and app-scoped
credentials apply from the next build on.
One practical note per platform: an Android keystore is app-agnostic, so a
single organization-wide keystore genuinely covers every Android and
Android TV app. An iOS or tvOS credential also carries a provisioning
profile, and profiles are tied to a bundle id — so an organization-wide
Apple credential only works across multiple apps if its profile is a
wildcard one. With app-specific profiles (the common case), scope each
Apple credential to its app with --app.
A build signs with exactly one credential. If several exist in the same
scope for the same platform, the first unexpired one in alphabetical name
order is used — the same order vibeview credentials list shows them in.
So if you keep both dist-2024 and dist-2025 organization-wide and both
are valid, builds sign with dist-2024; once dist-2024’s certificate
expires, builds switch to dist-2025 automatically. To control which one
is used, either keep a single credential per platform per scope, or scope
the one you want to the app with --app.
Expiry
Signing credentials expire. An Apple credential carries two expiry dates: the distribution certificate’s and the provisioning profile’s. Both are shown, and expiry warnings and refusals track whichever comes first. Profiles typically expire sooner than certificates (profiles usually last a year at most, certificates around three), so the profile date is often the one that matters — when it passes, renew the profile and re-upload the credential under the same name, even if the certificate is still valid.
vibeview credentials list warns on any credential expiring within 30
days. An expired credential doesn’t stop your builds as long as another
valid one exists in the same scope — builds skip past it to the next valid
credential, as described above. Only when every credential in the scope
that applies to the build has expired does VibeView refuse the build
immediately — the error names the refused credential and the date it
expired (the earlier of its certificate and profile dates) — rather than
letting the build run and fail partway through. This includes the case
where an app’s own scoped credentials have all expired while a valid
organization-wide one exists:
the build is still refused, because the organization-wide credential is not
the identity you chose for that app. Upload a replacement in the same
scope, or remove the expired app-scoped credential so the organization-wide
one applies.
Previewing what a build will sign with
You don’t have to run a build to find out how these rules resolve. Open an app in the dashboard and go to its Build Settings tab: the Production signing card shows, for each platform, exactly which credential a production build of that app would use right now — with its scope and expiry state. Android and Android TV appear as a single row when they resolve to the same keystore, which is the usual case. A platform with nothing to sign with shows “No credential — production builds will be refused”, with a link to add one.
Starting a production build
vibeview build --cloud --production
The first time you run this for a platform, the CLI asks for the release
build command and where the finished artifact lands (for example, a Gradle
task that runs bundleRelease, producing an .aab), and saves the answer
to your project config so later production builds don’t ask again.
Getting your artifact
vibeview builds download <build-id> [--out <dir>]
Or download it from the Builds page in the dashboard. A successful production build’s artifact is kept until you delete it — the same retention as any other build.
Only successful builds produce a downloadable artifact. If a production build fails or times out, nothing is kept from it — the build stays in your history with its logs, but there is no file to download. Fix the problem and run the build again.
Deleting an artifact
Once you no longer need a production build’s file, you can delete it without losing the build’s history:
vibeview builds delete-artifact <build-id>
Or use the Delete action next to Download on the Builds page — you’ll be asked to confirm, since this can’t be undone. Deleting an artifact removes the downloadable file only; the build still appears in your build history with its logs and cost, so your records stay intact. If you need the file again, run a new production build — a deleted artifact can’t be regenerated.
Android: signing config
To point your Android project at the keystore VibeView provides during a
production build, add a signingConfigs.release block to
android/app/build.gradle that reads these four properties:
if (project.hasProperty('VIBEVIEW_STORE_FILE')) {
signingConfigs {
release {
storeFile file(VIBEVIEW_STORE_FILE)
storePassword VIBEVIEW_STORE_PASSWORD
keyAlias VIBEVIEW_KEY_ALIAS
keyPassword VIBEVIEW_KEY_PASSWORD
}
}
buildTypes.release.signingConfig signingConfigs.release
}
The hasProperty guard matters: these four properties are only set during a
VibeView production build. Without the guard, an ordinary local build (like
assembleDebug) fails immediately with MissingPropertyException before any
task even runs, because Groovy resolves bare property references at
configuration time. With the guard, the whole block is inert when the
properties are absent, so local builds are unaffected.
Keep the signingConfig assignment inside the guard, as shown. If you
wire buildTypes.release.signingConfig to signingConfigs.release
unconditionally, then whenever the properties are absent the release build is
pointed at a signing config with no keystore in it — which produces an
unsigned release build rather than an error. VibeView refuses such an
artifact when it supplied signing credentials for the build, so you get a
clear failure instead of a bundle that looks signed but isn’t; keeping the
assignment inside the guard avoids the situation altogether.
VibeView supplies these four values automatically during a production build — nothing else to configure.
Android key recovery
What happens if you lose an Android keystore depends on how your app is set up on Google Play:
- With Play App Signing (the default for new apps), the keystore you hold is an upload key — Google holds the actual app signing key and re-signs your app with it. If you lose or compromise your upload key, Google Play support can help you reset it.
- With legacy direct signing (older apps that opted out of Play App Signing), the keystore is your app signing key. If you lose it, that listing can never be updated again — there is no recovery.
Check which one applies to your app in the Google Play Console before you decide how carefully to back up your own copy of the keystore — VibeView doesn’t keep one for you either way.
iOS: signing config
Nothing to add to your Xcode project. VibeView reads the distribution type (App Store, Ad Hoc, in-house, or Development) directly from your uploaded provisioning profile, so a production build is signed and exported to match whatever kind of profile you uploaded.
What your build command must reference
VibeView provides four values to your iOS production build, all derived from the credential and profile you uploaded. Both steps of your build command need them — and leaving either set out fails the build late, after the archive has already been built and paid for.
The archive step needs to be told to sign manually, with your team, profile and certificate:
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath ios/build/MyApp.xcarchive \
CODE_SIGN_STYLE=Manual \
${VIBEVIEW_DEVELOPMENT_TEAM:+"DEVELOPMENT_TEAM=$VIBEVIEW_DEVELOPMENT_TEAM"} \
${VIBEVIEW_PROVISIONING_PROFILE:+"PROVISIONING_PROFILE_SPECIFIER=$VIBEVIEW_PROVISIONING_PROFILE"} \
${VIBEVIEW_CODE_SIGN_IDENTITY:+"CODE_SIGN_IDENTITY=$VIBEVIEW_CODE_SIGN_IDENTITY"}
Without these, Xcode falls back to automatic signing, which needs a signed-in Apple ID that a build machine doesn’t have — so the archive fails with a message about a missing signing certificate or development team, even though your certificate uploaded fine.
If your project uses CocoaPods (most React Native apps)
A build setting passed on the xcodebuild command line applies to every
target in the workspace, including every Pod. Pod library targets cannot
take a provisioning profile, so passing
PROVISIONING_PROFILE_SPECIFIER this way fails the archive:
error: React-featureflags does not support provisioning profiles, but
provisioning profile MyApp Appstore has been manually specified.
Per-target signing belongs in your Xcode project, not on the command line.
If your app target already has CODE_SIGN_STYLE = Manual, a
DEVELOPMENT_TEAM and a PROVISIONING_PROFILE_SPECIFIER in
project.pbxproj — which is the usual setup for a project that ships to the
App Store — then pass none of these on the archive command. Your
project’s own settings are already correct, and the only thing the archive
step needs from VibeView is nothing at all:
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-configuration Release \
-archivePath ios/build/MyApp.xcarchive \
${VIBEVIEW_CODE_SIGN_IDENTITY:+"CODE_SIGN_IDENTITY=$VIBEVIEW_CODE_SIGN_IDENTITY"}
CODE_SIGN_IDENTITY is safe to pass workspace-wide (Pods accept an identity;
only a profile is rejected) and is worth passing: a project that names the
legacy iPhone Distribution identity resolves it against a developer’s
keychain but not necessarily against the clean keychain a build host creates
from your uploaded .p12.
The export step still needs $VIBEVIEW_EXPORT_OPTIONS_PLIST either way.
Note the quotes inside ${VAR:+"..."} in every example. Without them a
value containing spaces — and Apple Distribution: Your Company (TEAMID)
does — is split into separate arguments, and xcodebuild reports something
unrelated like Unknown build action 'Distribution'.
The export step needs the export options file VibeView writes for you, matched to the profile you uploaded:
xcodebuild -exportArchive \
-archivePath ios/build/MyApp.xcarchive \
-exportPath ios/build \
-exportOptionsPlist "$VIBEVIEW_EXPORT_OPTIONS_PLIST"
You never write that file yourself — VibeView generates it from your provisioning profile at build time, including the mapping from your app’s bundle identifier to the profile that covers it.
When VibeView prompts you for a production build command and can detect your Xcode workspace, the command it offers already includes all of the above. If you write your own command, add them yourself.
The ${VAR:+"..."} form is deliberate: if VibeView can’t determine one of
these values from your profile, that setting drops out of the command
entirely and your project’s own setting stands, rather than being overridden
with a blank.
Exporting your certificate
Export the .p12 from Keychain Access on a Mac, or from Xcode — both
produce a file iOS builds can read.
If you generate the .p12 yourself with a recent version of the openssl
command-line tool, add the -legacy flag:
openssl pkcs12 -export -legacy -out cert.p12 -inkey key.pem -in cert.pem
Recent OpenSSL versions default to an encryption format that macOS cannot
open. Without -legacy the build fails while reading the certificate, and
the underlying error reports a password problem even when your password is
correct — so if you are certain the password is right, re-export with
-legacy before assuming anything else is wrong.