Migrating from a Legacy Ridy Taxi Platform

The official BetterSuite migration playbook for tenants leaving a first-generation TypeORM-style MariaDB taxi platform (the family commonly called Ridy). End-to-end: discovery, dry-runs, app-store release, and the cutover.

Last updated May 19, 2026

This is the only fully supported, end-to-end migration path BetterSuite ships today. The provider is called legacy_ridy_taxi and it knows the TypeORM-shaped MariaDB schema used by first-generation Ridy-script taxi platforms (driver, rider, request, payment, media and friends). If your legacy app is on that family — including BlackCab-class clones — this is the right page.

If you're on Uber Fleet, Yelo, Tookan, or another SaaS competitor, BetterSuite doesn't have a database-level importer for you — see Migrating from Uber Fleet and Migrating from Yelo for the honest "relaunch" guidance.

What the runner does

legacy_ridy_taxi connects to your legacy app's MySQL/MariaDB, reads the schema phase by phase, and writes the equivalent rows into your BetterSuite Postgres. It runs as a multi-phase pipeline — 21 phases today, in dependency order:

tenant → reference → uploads → accounts → drivers → credentials → saved_locations → kyc → wallets → psp_accounts → operators → providers → promotions → orders → payments → reviews → feedback → chat → support_tickets → zone_pricing → reputation

Each phase reads its slice, transforms it, writes target rows, and updates an internal id_map so later phases can resolve foreign keys. The run is wrapped in a single SQL transaction (per phase) and supports dry-run mode that rolls everything back, and incremental re-runs that use a last_synced_at watermark so you only re-read changed source rows.

For blob assets (driver photos, KYC documents, in-chat images), the uploads phase mirrors media rows into BetterSuite's uploads table and either copies the bytes into your tenant's storage backend, or — if you've toggled copy_blobs off — just mirrors the metadata so you can run blob copy as a separate pass.

What you need before you start

  • A read-replica of your legacy MariaDB, ideally. The runner is read-only against the source, but production-source migration during peak hours can hurt your legacy app's latency.
  • MySQL connection URL in the form mysql://user:pass@host:3306/dbname.
  • Egress to the database from BetterSuite. Two options, picked per-source:
    • Direct — BetterSuite's egress IPs dial your DB host. Requires the DB to be reachable from the internet (firewall rules, etc.).
    • SSH tunnel — BetterSuite SSH's to a bastion you control, then opens a direct-tcpip channel to the DB host. The DB stays inside your VPC. You'll need an SSH user, a private key (and optional passphrase), and the bastion's host-key SHA256 fingerprint. The fingerprint is TOFU on first connect (stored automatically) and fails closed on mismatch afterward.
  • The legacy app's .env file (or just its content). The migrations dashboard parses this client-side and stores the relevant keys in a legacy_env_import blob on the source — that's how the runner auto-resolves things like STORAGE_DRIVER, AWS_S3_BUCKET, and the local upload root without you typing them in twice.
  • Access to the legacy app's upload tree:
    • Local — the path on disk where the legacy app stored uploads (/app/uploads by default in many Ridy-family deployments). The migration host needs read access to this path, either by sharing the volume or rsync'ing it across.
    • S3 — the S3 bucket name and credentials. S3-compatible (R2, MinIO) is supported; set the endpoint URL and the force_path_style flag for non-AWS services.
  • A new BetterSuite tenant in test mode. Finish Sign Up and pick a plan before you start the migration setup.

Step 1: Create the migration source

In your BetterSuite Owner Dashboard → Migrations → New migration:

  1. Give the source a name (shown in the migrations list and run history — e.g. Production or Pilot).
  2. Pick Legacy Ridy taxi platform as the provider (it's currently the only option).
  3. Click Create. You land on the source's configure page.

The newly-created source starts in ACTIVE status with empty config and no secrets.

Step 2: Import the legacy .env

On the configure page, paste the legacy app's .env content (the form has a dedicated Import .env action) or upload the file. The parser is client-side; you can review what it pulled out before saving.

The import is split into two channels:

  • Secrets (mysql_url, S3 credentials) — encrypted and stored in data_sync_sources.encrypted_secrets. Never returned to the browser after save.
  • Config defaults — stored under legacy_env_import. The runner's config layer resolves operator > env > host-defaults, so anything you don't override falls back to the parsed env value.

This step usually saves a lot of typing — most of the storage and DB-host fields come straight from the .env.

Step 3: Answer the discovery questions

The dashboard then runs the provider's discovery routine, which lists the operator-decision questions. These are the ones the .env can't answer for you:

QuestionRequiredDefaultWhat it controls
default_countryGBISO-3166 alpha-2 code used when a source row's countryIso is NULL.
code_prefixLEGACYPrefix on imported order codes (e.g. LEGACY-1234) and support ticket numbers.
kyc_policy_keylegacy-driver-v1Slug for the imported KYC policy. Change only if you have multiple sources.
currency_policystrictstrict aborts on any non-base-currency row; filter skips them and counts them under skipped_by_reason; convert applies per-currency exchange rates you supply (originals preserved as metadata).
copy_blobsenabledenabled copies bytes during uploads; skipped mirrors metadata only — useful for fast iteration.
source_storage_kind(from .env)local or s3.
source_local_base_path / s3_bucketconditional(from .env)Where the legacy app stored its uploads.
connection_modedirectdirect or ssh_tunnel. SSH adds ssh_host, ssh_port, ssh_user, ssh_known_fingerprint.

You'll also fill in:

  • car_catalog — a mapping table from each legacy car.id to a BetterSuite (make, model) pair. The legacy schema flattens these into a single string; BetterSuite splits them. The form pre-fills entries from your legacy car table so you only edit the splits.
  • kyc_doc_types — a mapping from each legacy driver_document.kindId to a BetterSuite kyc_check_kind (the reference list is shown alongside).
  • color_hex_defaults — hex values for any color names the legacy app uses that the BetterSuite catalog doesn't know. Optional; missing colors get NULL hex.

Save. The source now has enough config and secrets to attempt a connection.

Step 4: Dry-run

The configure page has two run buttons: Run and Dry run. Always start with a dry-run. It does the full pipeline but:

  • Rolls back every SQL transaction at the end of each phase (target Postgres is untouched).
  • Skips the byte copy in the uploads phase, even if copy_blobs = enabled (the target storage is irreversible — the runner never writes there during a dry-run).
  • Produces the same per-phase metrics (input_rows_read, output_rows_written, skipped_by_reason) as a real run.

Open the resulting run from Migrations → [source] → Runs → [run]. The detail page shows each phase, its row counts, and any errors. Use this to:

  • Verify your default_country is sensible (look at the share of phone numbers that needed canonicalization fallback).
  • Sanity-check your currency_policy choice (the filter skipped count is a good signal).
  • Spot any unmapped car.id values (they'll surface as warnings).
  • Confirm the SSH tunnel works end-to-end — the first dry-run with a tunnel writes the discovered host-key fingerprint into your source config (TOFU).

Iterate on config and dry-run a few times. The runner is idempotent and incremental, so re-running just picks up where the watermark left off.

Step 5: A first non-dry-run

When dry-runs look clean, click Run. This does the same pipeline, this time committing the SQL transactions and copying blob bytes. Expect it to take longer than the dry-run — the bytes are the slow part.

After the first non-dry-run, your BetterSuite tenant has a real, queryable copy of your legacy data. Drivers, riders, vehicles, KYC applications, completed trip history, and reviews are all present. You can sign in to the Ops Console and click around to verify.

This is not cutover yet. Your legacy app is still the source of truth. The next sections cover how to actually move the wheel.

Step 6: Prepare the new build for app stores

Your customers and drivers don't talk to the database — they talk to whatever app you ship them. BetterSuite uses different apps from the legacy platform, so cutover is conceptually:

  1. Get the new BetterSuite-based app builds (driver + passenger) reviewed and approved by Apple App Store and Google Play before cutover day.
  2. Cut over the data at the chosen time and force the new build on existing users in the same window.

Use Owner Dashboard → Store Release to kick off the branded builds for the App Store and Play Store (Pro+ feature — see Plans & Billing). App-store review usually takes a few business days; build that time into your plan.

Critically: submit the builds while the migration is still in dry-run / test mode, so review and approval happen in parallel with you tightening the migration config. By the time you're confident in the data, you want the new build sitting in the store, ready to be released.

Step 7: Pick a low-traffic cutover window

The cutover window is the period during which:

  • Your legacy app stops accepting new writes.
  • The migration runner does a final incremental pass to catch any last-minute changes.
  • You release the new app build from store review.
  • You force-update legacy clients to the new version.

You want this window to land on the lowest-traffic time you reasonably have — for most fleets that's a weekday off-peak hour (e.g. 02:00–05:00 local), or a public-holiday morning. The fewer in-flight trips, the fewer awkward "this trip exists in both systems" cases you'll have to reconcile.

Step 8: The cutover

Roughly in order on cutover day:

  1. Tell drivers and operators it's happening an hour ahead — push notification, WhatsApp / Telegram group, in-app banner. The clearer the message, the less support load when the apps switch under them.
  2. Stop accepting new dispatches in the legacy app. Most Ridy-family deployments don't have a clean "drain" switch — operationally you put the dispatcher in pause / "manual only" mode so no new trips start. Let in-flight trips finish in the legacy system.
  3. Run incremental migration one more time from Owner Dashboard → Migrations → [source] → Run. This is a real run, not a dry-run, and picks up any rows changed since your last run via the watermark. Because the legacy app has stopped writing, this is the final snapshot.
  4. Verify counts on the run detail page. The new run should have a small input_rows_read (just the delta) and zero unexpected skipped_by_reason.
  5. Release the app-store build from store review (click "Release" in App Store Connect and / or Google Play Console). With many tenants we've seen the release roll out to the store within minutes.
  6. Force-update legacy clients to the new build. First-generation Ridy backends expose minimum-version environment variables that the legacy app checks on every API call — set these to a version higher than any shipped legacy build, and the legacy clients show a hard update screen pointing at your new app-store listing. Refer to the legacy app's own docs for the exact variable names (typically MIN_ANDROID_VERSION / MIN_IOS_VERSION or similar).
  7. Switch the operators over by sending them the new admin console URL — <your-slug>-admin.bettersuite.io (or your custom admin domain on Pro+). Their existing operator records are in the imported data, but they'll re-onboard themselves with new passwords / passkeys.
  8. Keep the legacy app online for read access during the dispute window. Refunds and chargebacks tied to pre-cutover trips have to be handled in the system that processed them. Most tenants leave the legacy stack running, read-only, for ~60 days.

What you can re-run after cutover

The migration runner doesn't "lock" after a successful run — it keeps its watermark and can be re-run for a long time if you discover something. Practical examples:

  • You forgot to set copy_blobs = enabled on the first big run. Flip the toggle and re-run; only the missing bytes are copied.
  • A driver complaint surfaces a missing trip from 2024. Re-run the orders and payments phases against the legacy DB (still running read-only) — the runner notices the rows are now present and pulls them.
  • You discover an kyc_doc_types mapping was wrong. Fix the mapping in config, re-run the kyc phase, and watch the application records update in place (the id_map prevents duplicates).

You'd typically stop re-running once you've decommissioned the legacy database.

Operational details

  • Cancellation — long runs can be cancelled from the run detail page. Each phase polls a cancellation flag (with a 2-second TTL cache) and returns cleanly at the next row boundary. The SQL transaction rolls back, leaving target Postgres in its pre-run state.
  • SSH session lifetime — when connection_mode = ssh_tunnel, the SSH session stays open for the full duration of the run. If your bastion drops the connection mid-run, the run fails — pick a stable bastion (and a reasonable TCP keepalive on it).
  • Currency convert mode — uses the exchange rates you supply in config. The original currency and amount are preserved on the target row's metadata for accounting / audit.
  • The id_map — every phase that creates target rows records (legacy_table, legacy_id) → target_uuid. Subsequent phases look up FKs through this map. The map is per-source, so two sources never collide.
  • Per-row skip reasons — when a row can't be migrated (e.g. a driver with no phone number, a non-base-currency payment under filter policy), the runner increments a per-reason counter shown on the phase detail. No silent drops.

What's next

  • Plans & Billing — confirm your plan covers your post-cutover scale (locations, transactions/month).
  • Dashboard Tour — the owner dashboard, including where Migrations lives.
  • Onboard Drivers — imported drivers still have to sign back in. They'll go through the BetterSuite login flow (phone + OTP) the first time.
  • Stripe Payments — drivers re-onboard payouts via Stripe Connect Express; the legacy payout config doesn't carry over.

Build the foundation once. Expand without limits.

BetterSuite is built for teams who see on-demand as a business — not a feature.