Skip to content
DevLaunch home

Guide · CRM & customer operations

Connect, Brand, and Launch Your Seedly CRM Mobile Apps

A practical launch guide for the GoSeedly Mobile App Pack: connect your CRM, verify sign-in and push, customize your brand, and release iOS and Android apps.

By DevLaunchPublished

Start with a working CRM connection, prove the app on a real device, and work through branding, push notifications, and store release in order.

Leg 1 · Connect

Three addresses, one login, one terminal check before anything is built. Do the stations in order; each ends with a check you can see.

Gather the three addresses and a login

Everything downstream is a copy-paste of these. Get them right once.

AddressWhat it isWhere to find it
Deployment URL, https://<name>.convex.cloudWhere the data livesConvex dashboard → project → production → Settings → "Deployment URL". Also the CRM's NEXT_PUBLIC_CONVEX_URL.
Site URL, https://<name>.convex.siteWhere sign-in livesSame page, "HTTP Actions URL". The deployment URL with .cloud swapped for .site.
Origin, https://crm.yourcompany.comYour dashboard's addressThe CRM's SITE_URL environment variable. In your CRM checkout, npx convex env get SITE_URL --prod prints it.

You need

A normal CRM user with access to at least one location, their authenticator app if the role uses two-factor, and the CRM version from its changelog so you can record what you tested against.

Prove the front door from a terminal

Ten seconds, no build, and it finds the Origin mistake before an app can.

Do this

SITE_URL=https://<name>.convex.site \
ORIGIN=https://crm.yourcompany.com \
EMAIL=you@yourcompany.com PASSWORD='…' TOTP=123456 \
ios/Scripts/contract-check.sh

Check

A sign-in response, then a token whose claims print. That means the CRM accepted the login from that Origin and handed back what the apps need.

If not

You seeIt meansDo
403 or "Invalid origin"The Origin is not one the CRM trustsUse the CRM's SITE_URL exactly
"don't match"CredentialsSign in on the web with the same details first
Asked for a codeThe role needs two-factorCodes last 30 seconds. Get a fresh one and run again
429Five attempts a minute per addressWait a minute

Choose how to connect

One phone, a whole team, or a build machine. Same three addresses, three places to put them.

SituationWhere the addresses goRebuild?
Trying it on your own phoneIn the app: More → You → Connect to a CRM. Paste, save, relaunch.No
Everyone at the companyBaked into the build with Scripts/connect.sh. Every install of that build talks to your CRM; people still sign in as themselves.Yes
CI, or a developer's schemeEnvironment variables CRM_DEPLOYMENT_URL, CRM_SITE_URL, CRM_ORIGIN, CRM_RELAY_URLYes

Do this

For a team, from the repo root:

Scripts/connect.sh \
  https://<name>.convex.cloud \
  https://<name>.convex.site \
  https://crm.yourcompany.com \
  5.8.4

# then prove sign-in through the app's own code path
EMAIL=you@yourcompany.com PASSWORD='…' TOTP=123456 Scripts/connect.sh --check

It validates the addresses and writes one identical file into both apps, ios/Config/Deployment.json and android/app/src/main/assets/Deployment.json. Both are ignored by git and must stay that way.

Sign in and walk the app

Rebuild, install, and put the app through a real morning before anyone else does.

  • Inbox shows real conversations, and a new inbound message appears without refreshing.
  • An SMS sent from a thread arrives on the test phone you expect.
  • Contacts, Deals and Today show your data. The location switcher lists every location you belong to.
  • More shows only the tools the CRM answers for. "Not available on this CRM" is the app being honest about the CRM version, not a fault.
  • Placing a call from a contact works if your location's dialer works on the web. If the dialer is off there, the app hands the call to the phone's own dialer.

If your CRM runs Seedly Dispatch

Nothing to configure in the app. It asks the CRM one question on sign-in and shows the field screens when the add-on answers.

  • Every technician is a CRM user and on the Dispatch roster. A login without a roster entry sees "not on the roster yet", not an empty day.
  • Technicians have dispatch_jobs; dispatchers have dispatch_schedule.
  • Android builds carry a Google Maps key (MAPS_API_KEY). iPhone needs nothing. Without a key the Android screens still work as lists with "Open in Google Maps".
  • For push on visit assignments, the outbox in the next station is set up on both sides.
  • A technician has walked one real stop: On my way, Arrived, a photo, Finished.

Push notifications, and calls that ring a closed phone

Optional. Everything else works without it. Budget an hour the first time.

The CRM's push is built for browsers. Phones need Apple's and Google's push services, so the pack adds two small pieces, both yours: a relay on your own Cloudflare account that receives the CRM's signed webhooks and forwards them to phones, and a Seedly add-on installed into your CRM through its extension registries that answers "who should be told" from each person's own notification settings and quiet hours. The add-on is also the only way an inbound call can ring inside the app while it is closed.

In order

  1. Deploy the relay by following goseedly-server/relay/README.md steps 1 to 7. Check: curl https://<your-relay>/health shows "ok": true.
  2. Subscribe the CRM's webhooks to it (step 8). Check: ./scripts/register-webhook.sh --list shows one subscription per location.
  3. Install the add-on with goseedly-server/seedly-addon/install.sh and deploy your CRM. Check: the README's three curls return 200 and 201.
  4. Give the apps the relay's address: the fifth argument to Scripts/connect.sh, or Connect to a CRM → Relay URL on one phone.
  5. Verify on a real device. You → Notifications → Send a test push should arrive within seconds. Simulators cannot receive push.
  6. If Dispatch is installed, turn on the outbox: OUTBOX_SECRET on the relay, GOSEEDLY_RELAY_URL and GOSEEDLY_RELAY_OUTBOX_SECRET on the CRM. Dispatch creates notifications without webhooks; the add-on's once-a-minute outbox carries them. Check: a visit assigned in the CRM reaches the phone within a minute.

Write it down

Two minutes now saves an afternoon when Seedly ships an update.

  • CRM version and today's date added to the compatibility matrix in docs/PRODUCT-READINESS.md and the pin at the top of CONTRACT.md.
  • Every tool that reported "not available on this CRM" listed, so nobody files it as a bug later.
  • git status shows neither Deployment.json.
  • The relay URL, if any, stored where the next person will find it, not in one engineer's shell history.

Leg 2 · Ship

Most of this is waiting: for an account to be approved, for a build to process, for a review. Almost none of it is work, and the order matters.

Two decisions before you spend a day on anything

Neither is technical. Both can stop a release.

First

The app has run against your real CRM. Leg 1, all of it, with real people on real phones for at least a few days. Shipping a listing for an app that has only ever seen sample data is the one way this goes badly in public.

Second

The name is yours. Market the app under your company's name and say it works with your CRM. Do not put a CRM vendor's trademark in the app name, subtitle or icon without written permission from them. GoSeedly is the codebase's name; your listing says what is yours to say.

Accounts and a domain

Start these first. Apple's takes about a week.

AccountCostRegister asWhy it matters
Apple Developer Program$99 a yearYour company, with a D-U-N-S numberAn individual account puts a person's name on the listing forever
Google Play Console$25 onceAn organisationPersonal accounts must run 12 testers for 14 days before publishing publicly
A domain you controlvariesYouPrivacy policy, terms and support pages must resolve at a real address; both stores check

Make it yours

One theme file, one identifier you can never change, one icon script.

WhatWhereThen
Name, colour, font, corner style, onboarding copyshared/Theme.jsonScripts/sync-theme.sh
Home Screen name, bundle ID, versionios/project.yml and android/app/build.gradle.ktscd ios && xcodegen generate
Icon, both platformsios/Scripts/make-icon.shCopy the Android set per android/scripts/icon/README.md
Which CRM the build talks toScripts/connect.sh, Leg 1Scripts/connect.sh --check

Screenshots and listings

Capture, frame, caption. Then the one field that gets apps rejected.

Do this

ios/Scripts/capture-all.sh                                # every screen, light and dark
ios/Scripts/frame-screenshots.sh docs/screenshots/<date>    # store sizes on your brand tint
android/scripts/capture.sh emulator-5554 docs/screenshots/android-<date>

The framing script writes every size both stores want: iPhone 6.9" and 6.3", Play phone and landscape, and the Play feature graphic. Captions come from ios/Scripts/store-captions.csv, eight hero screens in the order the store shows them. Edit that file, not the images.

Listings

Write both from docs/templates/app-store-listing.md and docs/templates/play-listing.md, side by side, so they say the same thing.

iPhone: archive, upload, TestFlight

One script. It refuses to start until it has what it needs and says what is missing.

export TEAM_ID=ABCDE12345                   # Apple Developer → Membership
export ASC_KEY_ID=X1Y2Z3W4V5                # App Store Connect → Users and Access → Integrations
export ASC_ISSUER_ID=69a6de70-…
export ASC_KEY_PATH=~/keys/AuthKey_X1Y2Z3W4V5.p8

ios/Scripts/release.sh --dry-run            # what it would do
ios/Scripts/release.sh                      # archive, export, validate, upload

Check

Processing takes 5 to 30 minutes. Then TestFlight, in front of real people on real phones, for at least a few days. Only then App Store, with the listing from the previous station.

Android: sign, bundle, Internal testing

Make one upload key, back it up like a deed, and always test the release-signed build first.

keytool -genkeypair -v -keystore ~/keys/yourcompany-upload.jks \
  -alias upload -keyalg RSA -keysize 4096 -validity 10000

export KEYSTORE_PATH=~/keys/yourcompany-upload.jks
export KEYSTORE_PASSWORD='…'
export KEY_ALIAS=upload

android/scripts/release.sh --dry-run
android/scripts/release.sh                  # → app/build/outputs/bundle/release/app-release.aab

Check

Upload the .aab to Internal testing first, always. It is live in minutes on real devices, signed with the release key, which catches the whole class of problem a debug build hides. Then closed testing, then production.

A remote, a green CI run, then submit

A workflow that has never been green is not a workflow.

gh repo create yourcompany/yourapp --private --source=. --remote=origin
git push -u origin main

The pack's .github/workflows/ci.yml builds both apps, runs the unit tests, and runs the wall check that proves no CRM code has entered the repo. Protect main so it has to pass before anything merges. Watch the first run.

Before you submit

  • The app has run against a real CRM deployment, not just sample data.
  • Naming settled. A vendor's trademark is not in the name, subtitle or icon.
  • Apple and Play accounts in the company's name.
  • Bundle ID and application ID decided. They are permanent.
  • Theme synced, app name set, icon generated and copied on both platforms.
  • Privacy manifest re-read. Privacy policy and support page live at real URLs.
  • Screenshots framed and looked at small. Both listings written. Demo-account note pasted in.
  • A TestFlight build and an Internal testing build in front of at least one person who is not you.
  • Git remote created and CI green.
  • Someone other than you has read the privacy policy and the terms.

Keep building

View topic →