Skip to main content

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

PhaseOwnerSupporting roles
Scope and contractSales, ProductCustomer Success
Asset collectionCustomer SuccessClient marketing team
Play Console prepFUL DevProduct (as reviewer)
Tenant bootstrap (automated)FUL DevProduct (validates output)
Store listing and releaseFUL Dev, ProductQA
Ongoing maintenanceFUL DevCustomer 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:

  1. We store the uploaded files in a Cloudflare R2 bucket
  2. 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
  3. We automatically generate a normalized store-listing.json that 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:

  1. Fetches the client submission and images from the portal
  2. Clones our publicala/fenice/dev/tenants repository and creates a new branch named after the tenant slug. The branch contains the tenant configuration, icons (generated with sharp from the source 1024x1024), splash screens, theme, translations, and source assets
  3. Clones our publicala/fenice/dep/env/tenants-envs repository and creates a matching branch with environment specific values and shared environment variables pinned to the production branch of our fenice-env submodule
  4. 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
  5. Adds an entry to version.json in 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.

RepositoryPurposeBranch per tenant
publicala/fenice/dev/monorepoShared code, scripts, build pipelinesFeature branches only
publicala/fenice/dev/tenantsTenant specific assets and configuration (icons, splash, theme, translations)Yes, named after the tenant slug
publicala/fenice/dep/env/tenants-envsTenant specific environment variables, Firebase configs, keystore referencesYes, 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:

PhaseWall clock timeNotes
Asset collectionDepends on clientUsually the longest phase; we chase assets with Customer Success
Play Console app creation5 minutesManual, one time per tenant
Tenant bootstrap1 to 2 minutesFully automated by setup_tenant (includes Firebase)
Local validation30 to 60 minutesFirst time setup of pod install and SDKs can add an hour
Release build and upload45 to 90 minutesDominated by fastlane upload and Google Play review delays
Google Play review24 to 72 hoursOutside our control
Apple App Store review24 to 48 hoursOutside 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_tenant script 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 running setup_tenant or creating the apps manually with firebase 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 fmt library vendored by React Native (facebook/react-native#55601). The Podfile includes a workaround that forces C++17 for the fmt pod. If the error reappears after a pod install, verify the workaround is present in the post_install block.
  • LaunchScreen.storyboard rejected by Xcode. Older storyboard templates generated by setup_tenant used targetRuntime="AppleSDK" which Xcode 26 does not recognize. The template was updated to use targetRuntime="iOS.CocoaTouch" with proper device and dependency declarations. If a tenant has an old storyboard, regenerate it or copy the template from setup_tenant.js.
  • .npmrc missing for delfino download. The clone_reader script downloads @publicala/delfino from the GitLab Package Registry. This requires a .npmrc file at the monorepo root with the @publicala scope pointing to https://gitlab.com/api/v4/packages/npm/ and a valid deploy token. Without it, npm pack falls back to npmjs.com and fails with 404.
  • "Unable to resolve module" for tenant assets after branch switch. The file fenice/src/assets/requires.js is generated by create_env but was previously tracked in git. When switching branches, the committed version from a different tenant overwrites the correctly generated one. The fix (MR !124) removes requires.js from git tracking. If you see this error, run yarn create_env prod-env to regenerate it.
  • ReferenceError: "paddings" doesn't exist in get_tenant_styles. Earlier versions of the setup_tenant theme template destructured colors from Theme but referenced paddings.sm. The template was fixed to destructure paddings instead. If an existing tenant has this bug, update fenice/tenant/theme/index.js to use const { normalize, texts, paddings } = Theme.
  • yarn ios --simulator fails to parse simulator name. The run.js wrapper splits arguments on spaces, so --simulator "iPhone 17 Pro" is parsed as three separate arguments. Use npx react-native run-ios --simulator "iPhone 17 Pro" --scheme <Tenant>-Prod directly from the mobile/ 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.

X

Graph View