AutoFit is another coaching platform. This module moves a coach’s whole AutoFit account into a Perform studio: trainees, programs, history, sub-coaches, templates, nutrition, PDF plans, content, recipes and forms. The same lane re-syncs an account later, for all data types or a subset. The routes on this page only verify, start, report and resume. The migration itself runs in a BullMQ worker, workers/autofit-import-worker.ts, on the queue PerformQueue.AUTOFIT_IMPORT. A run pulls an entire account into the studio, so a sub coach cannot reach any of these routes and gets FORBIDDEN (insufficient role). The module is not a five-file module. It has routes (with inline handlers), a schema, a service and a repository, plus the AutoFit API client, mappers (AutoFit rows to Perform shapes) and writers (Perform rows to the database), which the worker uses.

How it works

Auth model

AutoFit issues one master export key that covers several coaches. Perform holds it as the server secret AUTOFIT_MASTER_KEY. A run picks one coach from the key’s coach list by phone number. The key alone would let any studio type another coach’s number and pull their clients. A one-time code sent to that phone over WhatsApp is the proof of ownership. No run starts without it. Phone numbers are reduced to AutoFit’s coach_mobile form before matching: digits only, with no 972 country code and no leading zero. In responses a number is only ever shown masked, such as 054-***-5848.

Two lanes

Both lanes end in the same launchRun, so a run started from either is the same thing.

Data types

A run covers any subset of ten data types. Every flag defaults to true.

Stages

A run’s status is its current stage. The worker walks only the stages its flags need (planOf), in this order: DONE follows the last stage in the plan. A run that fails moves to FAILED and records the stage it stopped in. A recipes-only re-sync is PULLING, WRITING_LIBRARY, DONE. The schema’s IMPORT_STAGES also lists MATCHING and REVIEW. In the worker code read for this page nothing sets a run to REVIEW. The two review routes below only act on a run in that status, so they appear to be unreachable in the current flow. This was not confirmed beyond the worker and this module.

Safety rules in the worker

These come from the worker’s header comment and explain behaviour you will see in the data:
  • Writes go straight through Prisma with no notifier, so an import can never send a WhatsApp or push to a trainee.
  • A trainee’s identity is externalUserId set to autofit:<id>, unique per studio, with a normalized phone fallback.
  • A trainee who existed in Perform before the import keeps their live status and dates. AutoFit only fills the profile.
  • Raw pulls are sealed with AES-GCM before they are written to R2, and the snapshot prefix is deleted when the run is done. When a new run starts, the snapshots of the studio’s earlier failed runs are deleted too.
  • Programs, templates, plans and form rows are keyed by the AutoFit id they carry in content.meta, so a re-run updates only rows the import wrote. See the AutoFit keys section on the Program templates page.

Limits

All counters live in Redis under perform:autofit-* keys with a 600 second window, unless noted. Codes and attempt counts are scoped per studio and phone, so another studio naming the same number can neither burn this studio’s code nor use up its sends.

Failure reasons

Most refusals carry a machine-readable reason in details.reason, next to the error code. The web side picks its copy by it.
The in-memory /ping limit throws RATE_LIMITED with no details.reason.

Ops master code

AUTOFIT_IMPORT_MASTER_CODE is an optional code that passes verification for any coach, so the team can run a migration for a customer without their phone in the loop. It is off when empty or shorter than six characters. It is compared in constant time, never consumes a pending code, counts against the code check limits, and every use is logged as a warning (autofit import: master code used). TRAINEE_OTP_DEV_MODE skips the WhatsApp send and returns the code in the response as devCode, for local testing.

Settings wizard endpoints

POST /v1/web/autofit-import/ping

Checks whether a phone number belongs to an AutoFit coach under the master key. Auth: web lane, OWNER or HEAD_COACH.
string
required
Trimmed, 6 to 20 characters. Normalized on the server.
Response:
Errors: RATE_LIMITED, and the reasons INVALID_PHONE, AUTOFIT_COACH_NOT_FOUND, AUTOFIT_UNAVAILABLE.

POST /v1/web/autofit-import/request-otp

Sends the verification code to the coach’s phone over WhatsApp. Auth: web lane, OWNER or HEAD_COACH.
string
required
Trimmed, 6 to 20 characters.
The service checks the studio is not a legacy migration, applies the lookup limit, resolves the coach, then issues a six-digit code. The code is stored in Redis for 300 seconds with a fresh attempt count and sent through the SmartSend template registercode in Hebrew. Response:
In dev mode the object also has devCode. Errors: reasons LEGACY_MIGRATION, RATE_LIMITED, INVALID_PHONE, AUTOFIT_COACH_NOT_FOUND, AUTOFIT_UNAVAILABLE, SEND_FAILED.

POST /v1/web/autofit-import/start

Verifies the code and starts a run. Auth: web lane, OWNER or HEAD_COACH.
string
required
The phone the code was sent to.
string
required
4 to 8 digits.
object
The ten data type flags. Each defaults to true, and the whole object defaults to all on.
In order: refuse when a run is active, refuse a legacy migration, check and spend the code, resolve the coach, then launch the run. Launching a run (launchRun):
  1. Creates the AutofitImport row with status PULLING, the coach name, the normalized phone, the client count, and counters holding include, the stage plan, the kind (resync when the studio already finished a run, otherwise import) and startedBy.
  2. A partial unique index allows one non-final run per studio. If two starts race, the slower one gets ALREADY_RUNNING.
  3. Enqueues the job run with the import id on the AUTOFIT_IMPORT queue.
  4. Deletes the R2 snapshots of the studio’s earlier failed runs.
Response:
Errors: reasons ALREADY_RUNNING, LEGACY_MIGRATION, RATE_LIMITED, INVALID_CODE, CODE_BURNED, CODE_EXPIRED, INVALID_PHONE, AUTOFIT_COACH_NOT_FOUND, AUTOFIT_UNAVAILABLE. VALIDATION (422) for a malformed body, such as a code that is not 4 to 8 digits.

GET /v1/web/autofit-import/status

The latest run in full, for the settings page and its report. Auth: web lane, OWNER or HEAD_COACH. No parameters. Returns { "run": null } when the studio has never run an import. Response:
The counter and report keys in the example are a subset. Both objects are written by the worker.
  • counters is the worker’s live bookkeeping with two keys removed: coachReport (sealed credentials) and phoneMarks.
  • report is set when the run is done.
  • Sub-coach passwords. The import creates logins for sub-coaches with a temporary password. Reports store those passwords sealed. presentReport opens them with BETTER_AUTH_SECRET for an owner or head coach, which is every caller of this router. A password that fails to open, for example after the secret was rotated, reads as null.
  • credentials merges the sub-coach rows of every finished run, newest first, one per coach (by email, else name and phone). A row that still carries a password wins over a later one that does not. It is returned whatever the latest run’s status, so a running or failed re-sync never hides the credentials from the first import.
Errors: UNAUTHORIZED, FORBIDDEN.

GET /v1/web/autofit-import/matches

Lists the exercise matches of a run that is waiting for review. Auth: web lane, OWNER or HEAD_COACH. Requires the studio’s active run to have status REVIEW. See the note under Stages about whether that status is reachable. Response:
Errors: CONFLICT (no import is waiting for review).

POST /v1/web/autofit-import/review

Submits the coach’s decisions on exercise matches and continues the run. Auth: web lane, OWNER or HEAD_COACH.
object[]
required
At most 2000 entries.
string
required
The match id. Must belong to the run under review.
string
required
confirm, create or link.
string
Required when action is link. An exercise the studio can see.
Decisions are applied one at a time, with no transaction. A failure part way through leaves the earlier decisions saved. After the loop, every match still PROPOSED is set to CREATE_NEW, the run moves to WRITING_PROGRAMS, and the job is enqueued again. Response:
Errors: CONFLICT (no import is waiting for review). BAD_REQUEST for unknown match in review, a confirm with no proposal, a proposal that no longer exists, link decision without an exercise, or picked exercise not found. VALIDATION.

POST /v1/web/autofit-import/resume

Resumes the latest run after a failure. Auth: web lane, OWNER or HEAD_COACH. No body. The latest run must be FAILED, and the stage it failed in (counters.failedStage) must be one of PULLING, WRITING_LIBRARY, IMPORTING, MATCHING, WRITING_PROGRAMS. The run’s status is set back to that stage, its error is cleared, and the job is enqueued again. No new code is needed. Response:
Errors: CONFLICT (no failed import to resume, or this import cannot be resumed).

Connect endpoints

POST /v1/web/autofit-import/connect/request-otp

Sends the verification code, without revealing whose account the number is. Auth: web lane, OWNER or HEAD_COACH.
string
Optional. null or a blank string counts as not sent.
The phone to prove is chosen in this order:
  1. The phone in the body. This is the “AutoFit is on a different number” path.
  2. The AutoFit account the studio synced before, so a re-sync asks the same account again.
  3. The phone on the caller’s own coach row, the one they verified at signup.
Unlike the settings lane, nothing about the coach comes back before the code is verified. Response:
In dev mode the object also has devCode. Errors: reasons LEGACY_MIGRATION, INVALID_PHONE, NO_SIGNUP_PHONE, RATE_LIMITED, AUTOFIT_COACH_NOT_FOUND, AUTOFIT_UNAVAILABLE, SEND_FAILED.

POST /v1/web/autofit-import/connect/verify

Checks the code and leaves a verified marker for the studio. Auth: web lane, OWNER or HEAD_COACH.
string
Optional. Must match what was used in the request step. The same fallback order applies.
string
required
4 to 8 digits.
The coach is resolved before the code is spent, so an AutoFit hiccup at this point leaves the code valid for a retry. On success a marker with the phone, the coach id and name, the client count and the time is stored in Redis at perform:autofit-verified:<studioId> for 30 minutes. That is long enough to read the preview and pick data types. A new verification replaces the marker. Response:
Errors: reasons INVALID_PHONE, NO_SIGNUP_PHONE, RATE_LIMITED, INVALID_CODE, CODE_BURNED, CODE_EXPIRED, AUTOFIT_COACH_NOT_FOUND, AUTOFIT_UNAVAILABLE. VALIDATION.

GET /v1/web/autofit-import/connect/preview

Counts of what the import would bring, and what is already in the studio, for the confirm screen. Auth: web lane, OWNER or HEAD_COACH. Requires a verified marker. No parameters. The preview asks AutoFit at most once per section, with no retries and an 8 second timeout. A busy or slow AutoFit means “no number”, not an error.
  • counts.trainees comes from the marker.
  • trainingPlans, nutrition and pdfPlans come from the paged client listing. They are null for an account with more than 3000 clients, or when the listing fails.
  • coaches, coachesWithoutEmail, templates, contents and recipes come from one library export. When a section came back whole, the import’s own mappers count it, so the number matches what a run would write. Past the row limit, the table’s total stands in as an upper bound.
  • coachesWithoutEmail counts sub-coaches that cannot become a login and will be skipped.
  • forms and history are always null. Only a full export shows them.
  • existing says what earlier runs already put in the studio, per row.
  • partial is true when any asked count is null.
  • isResync is true when the studio already finished a run.
The preview is cached in Redis under a key tied to the marker, for the rest of the marker’s life. A partial preview is rebuilt once it is 60 seconds old. Response:
Errors: reason NOT_VERIFIED.

POST /v1/web/autofit-import/connect/start

Starts a run from the verified marker. Auth: web lane, OWNER or HEAD_COACH. Requires a verified marker.
object
The ten data type flags. All default to true.
boolean
default:"false"
Set to true to confirm that a second AutoFit account is meant to land in this studio.
Checks run in this order: an active run, a legacy migration, the marker, then the account check. If the studio synced a different AutoFit number before and confirmOtherAccount is not true, the request is refused with OTHER_ACCOUNT. This stops a mistyped number from merging a second account into the studio. The marker is kept, so the confirmed retry needs no new code. On success the run is launched exactly as in the settings lane, and the marker and its preview cache are deleted. One verification gives one run. Response:
Errors: reasons ALREADY_RUNNING, LEGACY_MIGRATION, NOT_VERIFIED, OTHER_ACCOUNT.

Progress endpoint

GET /v1/web/autofit-import/sync

A slim view of the latest run, for the progress bar on the home page. Auth: web lane, OWNER or HEAD_COACH. No parameters. Every manager with the dashboard open polls this every few seconds, so it is one database read and a whitelist: numbers and flags only. It never carries the growing id lists or the sealed credentials. Returns { "run": null } when there has been no run. Response:
Errors: UNAUTHORIZED, FORBIDDEN.

Environment variables

Never put the value of any of these in documentation, tickets or logs.