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.
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.
| Address | What it is | Where to find it |
|---|---|---|
Deployment URL, https://<name>.convex.cloud | Where the data lives | Convex dashboard → project → production → Settings → "Deployment URL". Also the CRM's NEXT_PUBLIC_CONVEX_URL. |
Site URL, https://<name>.convex.site | Where sign-in lives | Same page, "HTTP Actions URL". The deployment URL with .cloud swapped for .site. |
Origin, https://crm.yourcompany.com | Your dashboard's address | The 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.shCheck
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 see | It means | Do |
|---|---|---|
| 403 or "Invalid origin" | The Origin is not one the CRM trusts | Use the CRM's SITE_URL exactly |
| "don't match" | Credentials | Sign in on the web with the same details first |
| Asked for a code | The role needs two-factor | Codes last 30 seconds. Get a fresh one and run again |
| 429 | Five attempts a minute per address | Wait a minute |
Choose how to connect
One phone, a whole team, or a build machine. Same three addresses, three places to put them.
| Situation | Where the addresses go | Rebuild? |
|---|---|---|
| Trying it on your own phone | In the app: More → You → Connect to a CRM. Paste, save, relaunch. | No |
| Everyone at the company | Baked 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 scheme | Environment variables CRM_DEPLOYMENT_URL, CRM_SITE_URL, CRM_ORIGIN, CRM_RELAY_URL | Yes |
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 --checkIt 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 havedispatch_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
- Deploy the relay by following
goseedly-server/relay/README.mdsteps 1 to 7. Check:curl https://<your-relay>/healthshows"ok": true. - Subscribe the CRM's webhooks to it (step 8). Check:
./scripts/register-webhook.sh --listshows one subscription per location. - Install the add-on with
goseedly-server/seedly-addon/install.shand deploy your CRM. Check: the README's threecurls return 200 and 201. - Give the apps the relay's address: the fifth argument to
Scripts/connect.sh, or Connect to a CRM → Relay URL on one phone. - Verify on a real device. You → Notifications → Send a test push should arrive within seconds. Simulators cannot receive push.
- If Dispatch is installed, turn on the outbox:
OUTBOX_SECRETon the relay,GOSEEDLY_RELAY_URLandGOSEEDLY_RELAY_OUTBOX_SECRETon 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.mdand the pin at the top ofCONTRACT.md. - Every tool that reported "not available on this CRM" listed, so nobody files it as a bug later.
git statusshows neitherDeployment.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.
| Account | Cost | Register as | Why it matters |
|---|---|---|---|
| Apple Developer Program | $99 a year | Your company, with a D-U-N-S number | An individual account puts a person's name on the listing forever |
| Google Play Console | $25 once | An organisation | Personal accounts must run 12 testers for 14 days before publishing publicly |
| A domain you control | varies | You | Privacy 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.
| What | Where | Then |
|---|---|---|
| Name, colour, font, corner style, onboarding copy | shared/Theme.json | Scripts/sync-theme.sh |
| Home Screen name, bundle ID, version | ios/project.yml and android/app/build.gradle.kts | cd ios && xcodegen generate |
| Icon, both platforms | ios/Scripts/make-icon.sh | Copy the Android set per android/scripts/icon/README.md |
| Which CRM the build talks to | Scripts/connect.sh, Leg 1 | Scripts/connect.sh --check |
Privacy and legal
The privacy manifest is written. The pages and questionnaires need your company's details.
ios/App/Resources/PrivacyInfo.xcprivacyre-read against today's dependency list. It declares no tracking and no collected data. An SDK that phones home makes it wrong, and a wrong manifest is a rejected upload.docs/templates/privacy-policy.mdfilled in and published at a stable URL on your domain.docs/templates/terms.mdpublished, or Apple's standard licence left in place.- Both store questionnaires answered from
docs/templates/app-privacy-answers.md. The two stores mean different things by "collect"; the page explains which is which. - A support page with the app's name, an email address and ideally a phone number. Reviewers open it.
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, uploadCheck
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.aabCheck
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 mainThe 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.