Branded App Setup Process
This document describes the end-to-end workflow we follow to deliver a new white label mobile application for a tenant. It covers every hand off between Sales, Product, Customer Success, and the FUL engineering team, from the moment we confirm the scope with the client until the app reaches production on the stores.
For the tactical command reference (exact CLI flags, file paths, and troubleshooting for engineers), see the custom-app-setup skill in the monorepo/.claude/skills/ directory. This page is the process view; the skill is the playbook.
When This Process Applies
We run this process whenever a tenant needs a dedicated branded app in the iOS App Store or Google Play Store, separate from the shared Publica Reader app. Typical triggers:
- A publisher signs a contract that includes a branded mobile app
- An existing tenant upgrades from Publica Reader to their own branded experience
- A pilot customer needs a custom build for internal validation before general release
If the tenant only needs web reader customization (colors, logo, domain), this process does not apply. See our White Label Applications overview for context on which tenants already run branded apps.
Roles and Hand Offs
| Phase | Owner | Supporting roles |
|---|---|---|
| Scope and contract | Sales, Product | Customer Success |
| Asset collection | Customer Success | Client marketing team |
| Play Console prep | FUL Dev | Product (as reviewer) |
| Tenant bootstrap (automated) | FUL Dev | Product (validates output) |
| Store listing and release | FUL Dev, Product | QA |
| Ongoing maintenance | FUL Dev | Customer Success |
The Product Manager is accountable for the full flow. FUL Dev owns the technical execution. Customer Success is the single point of contact with the client during asset collection to avoid back and forth between teams.
Phase 1: Client Asset Collection
We send the client a link to our asset collection portal at https://custom-app-request.publica.la. The portal walks the client through a four step form that captures:
- Company name, app name, and support email
- Target audience (children, adults, or both)
- Store listing texts per locale (title, short description, full description) with character limits that match Google Play requirements
- Required image assets (icon 1024x1024, icon 512x512, feature graphic 1024x500, splash screen 2732x2732, logo 664x168)
For a detailed breakdown of each asset (dimensions, format, safe zones), see Branded Apps Asset Requirements.
The portal supports seven languages (English, Spanish, Portuguese, German, French, Polish, Danish) so clients can fill the form in their own language. On submit:
- We store the uploaded files in a Cloudflare R2 bucket
- We post a Slack notification to the FUL channel with the submission summary and a direct link to create the app in the Google Play Console
- We automatically generate a normalized
store-listing.jsonthat we use later for fastlane metadata
Customer Success monitors the Slack notification and pings the client if any asset is missing or below spec.
Phase 2: Create Google Play Console App
Before we run the automated setup, a FUL engineer creates the Google Play Console app as a new entry in the Publica.la organization. We do not set the package name at this step; it is registered automatically on the first AAB upload. This is the only external account creation step that remains manual.
Firebase apps (one Android, one iOS) are created automatically by setup_tenant in Phase 3. We have four Firebase projects (fenice-staging, fenice-production, fenice-production-2, fenice-production-3) that tenants are distributed across; use --firebase-project <id> to target a specific project (defaults to fenice-staging).
Phase 3: Tenant Bootstrap
The FUL engineer runs a single command that provisions every artifact the tenant needs:
yarn setup_tenant <slug> --package la.publica.<slug> --tenant-id <farfalla-id>
Additional options: --portal-slug <name> (when the portal submission slug differs from the branch slug), --firebase-project <id> (target a specific Firebase project), --no-push (dry run without pushing to remotes), --skip-pull with --images-dir <path> (use local images instead of the portal).
This command lives in the fenice monorepo and performs the following work automatically:
- Fetches the client submission and images from the portal
- Clones our
publicala/fenice/dev/tenantsrepository and creates a new branch named after the tenant slug. The branch contains the tenant configuration, icons (generated withsharpfrom the source 1024x1024), splash screens, theme, translations, and source assets - Clones our
publicala/fenice/dep/env/tenants-envsrepository and creates a matching branch with environment specific values and shared environment variables pinned to the production branch of ourfenice-envsubmodule - Creates Firebase apps (Android + iOS) in the target Firebase project and downloads SDK configs (
google-services.json,GoogleService-Info.plist) into the tenant environment branch - Adds an entry to
version.jsonin the monorepo so the tenant appears in all downstream tooling (builds, version bumps, release notes)
At the end of this phase we have two live branches that any FUL engineer can check out locally to work on the new app. We do not write files directly into the monorepo; the monorepo just references the tenant branches via git submodules and the environment clone mechanism.
Phase 4: Local Validation
The engineer checks out the freshly created branches and generates the platform specific files:
yarn switch_tenant <slug>
This script clones the environment repository, checks out the tenant branch in the tenant submodule, and regenerates the local .env, Android resources, iOS schemes, Firebase configs, and the embedded Volpe reader bundle. After it finishes, the engineer runs the app on a simulator or physical device (yarn ios, yarn android, yarn ios-device) to validate:
- Icons render correctly at every density
- Splash screen shows the tenant logo centered on the safe zone
- Brand colors match the client brief
- Firebase push notifications initialize without errors
- Store listing texts appear in the correct locale
If something looks off, the engineer updates the tenant branch directly (for example, tweaks theme/index.js for brand colors or config/system/client.env for privacy and FAQ links) and re runs yarn create_env prod-env to regenerate derived files. Any change committed to the tenant branch propagates to builds via the same switch_tenant workflow.
Phase 5: Release Build and Store Upload
Once local validation passes, the engineer moves to release builds:
yarn build android prod-env # produces an AAB + APK
yarn build ios prod-env # produces an xcarchive + IPA
Before the first release build the engineer also places a production keystore at env/prod-env/client/<package>.keystore and adds the corresponding PUB_UPLOAD_* overrides to env/prod-env/client/app.env. Until that is done, debug builds run fine but release builds fail. We document the keystore setup inside the custom-app-setup skill because the values are sensitive.
The final uploads run through fastlane:
yarn publish:android # uploads the AAB to the internal track
yarn store_listing # generates fastlane metadata from the portal submission
yarn publish:metadata # uploads the listing texts and images
We start every release on the internal track so the QA team validates the build before we promote it through alpha, beta, and production:
yarn promote:android --from_track internal --to_track alpha
iOS releases follow the same pattern through TestFlight and App Store Connect.
Repositories Involved
The automation touches three GitLab repositories. Understanding the split helps when we need to debug or intervene manually.
| Repository | Purpose | Branch per tenant |
|---|---|---|
publicala/fenice/dev/monorepo | Shared code, scripts, build pipelines | Feature branches only |
publicala/fenice/dev/tenants | Tenant specific assets and configuration (icons, splash, theme, translations) | Yes, named after the tenant slug |
publicala/fenice/dep/env/tenants-envs | Tenant specific environment variables, Firebase configs, keystore references | Yes, named after the tenant slug |
A fourth repository (publicala/fenice/dep/env/fenice-envs) holds the shared environment variables that apply to every branded app. We keep this repository as a submodule of tenants-envs and pin each tenant branch to its prod branch HEAD during setup.
Timing and Expectations
On a happy path, the full pipeline runs in about half a working day once the client submits all assets:
| Phase | Wall clock time | Notes |
|---|---|---|
| Asset collection | Depends on client | Usually the longest phase; we chase assets with Customer Success |
| Play Console app creation | 5 minutes | Manual, one time per tenant |
| Tenant bootstrap | 1 to 2 minutes | Fully automated by setup_tenant (includes Firebase) |
| Local validation | 30 to 60 minutes | First time setup of pod install and SDKs can add an hour |
| Release build and upload | 45 to 90 minutes | Dominated by fastlane upload and Google Play review delays |
| Google Play review | 24 to 72 hours | Outside our control |
| Apple App Store review | 24 to 48 hours | Outside our control |
The two review windows at the end are our biggest source of uncertainty. We always plan releases with a one week buffer after the first submission in case the stores request changes.
When Things Go Wrong
The most common issues we see, in order of frequency:
- Assets below spec. The client uploads a logo with transparency or an icon smaller than 1024x1024. We fix this by re asking the client through Customer Success; we never upscale or edit client assets ourselves.
- Firebase app creation fails. The
setup_tenantscript creates Firebase apps automatically, but this can fail if the Firebase CLI is not installed, the engineer is not logged in, or the account lacks permissions on the target project. We recover by installing the CLI (npm install -g firebase-tools && firebase login), then re runningsetup_tenantor creating the apps manually withfirebase apps:create. - Firebase app in soft delete. If Firebase apps were previously deleted (e.g., recreating a tenant), the package name remains reserved for 30 days. Use a different Firebase project (
--firebase-project fenice-production-3) or wait for the reservation to expire. - Keystore not set up. The first release build fails because no production keystore exists. The engineer generates one and commits it to the tenant environment branch.
- Xcode build fails on fmt consteval error. Xcode 26 ships a stricter Clang that breaks the
fmtlibrary vendored by React Native (facebook/react-native#55601). The Podfile includes a workaround that forces C++17 for thefmtpod. If the error reappears after apod install, verify the workaround is present in thepost_installblock. - LaunchScreen.storyboard rejected by Xcode. Older storyboard templates generated by
setup_tenantusedtargetRuntime="AppleSDK"which Xcode 26 does not recognize. The template was updated to usetargetRuntime="iOS.CocoaTouch"with proper device and dependency declarations. If a tenant has an old storyboard, regenerate it or copy the template fromsetup_tenant.js. .npmrcmissing for delfino download. Theclone_readerscript downloads@publicala/delfinofrom the GitLab Package Registry. This requires a.npmrcfile at the monorepo root with the@publicalascope pointing tohttps://gitlab.com/api/v4/packages/npm/and a valid deploy token. Without it,npm packfalls back to npmjs.com and fails with 404.- "Unable to resolve module" for tenant assets after branch switch. The file
fenice/src/assets/requires.jsis generated bycreate_envbut was previously tracked in git. When switching branches, the committed version from a different tenant overwrites the correctly generated one. The fix (MR !124) removesrequires.jsfrom git tracking. If you see this error, runyarn create_env prod-envto regenerate it. - ReferenceError: "paddings" doesn't exist in
get_tenant_styles. Earlier versions of thesetup_tenanttheme template destructuredcolorsfrom Theme but referencedpaddings.sm. The template was fixed to destructurepaddingsinstead. If an existing tenant has this bug, updatefenice/tenant/theme/index.jsto useconst { normalize, texts, paddings } = Theme. yarn ios --simulatorfails to parse simulator name. Therun.jswrapper splits arguments on spaces, so--simulator "iPhone 17 Pro"is parsed as three separate arguments. Usenpx react-native run-ios --simulator "iPhone 17 Pro" --scheme <Tenant>-Proddirectly from themobile/directory instead.- Store rejection on first submission. Google Play or Apple rejects the submission for content rating, target audience mismatch, or missing privacy policy. We address the specific feedback and resubmit; IARC certificates can usually be reused across tenants of the same type.
For the technical recovery steps for each issue, see the troubleshooting section of the custom-app-setup skill in the monorepo.
Related Documentation
- Branded Apps Asset Requirements — what the client needs to provide
- White Label Applications — context on our branded app portfolio
- Communication of Apps — how we announce releases to end users
- Publica Reader Changelog — release history across all branded apps