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 secretAUTOFIT_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 samelaunchRun, so a run started from either is the same thing.
Data types
A run covers any subset of ten data types. Every flag defaults totrue.
Stages
A run’sstatus 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
externalUserIdset toautofit:<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 underperform: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 indetails.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.
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.
registercode in Hebrew.
Response:
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.launchRun):
- Creates the
AutofitImportrow with statusPULLING, the coach name, the normalized phone, the client count, andcountersholdinginclude, the stageplan, thekind(resyncwhen the studio already finished a run, otherwiseimport) andstartedBy. - A partial unique index allows one non-final run per studio. If two starts race, the slower one gets
ALREADY_RUNNING. - Enqueues the job
runwith the import id on theAUTOFIT_IMPORTqueue. - Deletes the R2 snapshots of the studio’s earlier failed runs.
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:
countersis the worker’s live bookkeeping with two keys removed:coachReport(sealed credentials) andphoneMarks.reportis 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.
presentReportopens them withBETTER_AUTH_SECRETfor 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 asnull. credentialsmerges 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.
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:
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:
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:
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 in the body. This is the “AutoFit is on a different number” path.
- The AutoFit account the studio synced before, so a re-sync asks the same account again.
- The phone on the caller’s own coach row, the one they verified at signup.
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.
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:
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.traineescomes from the marker.trainingPlans,nutritionandpdfPlanscome from the paged client listing. They arenullfor an account with more than 3000 clients, or when the listing fails.coaches,coachesWithoutEmail,templates,contentsandrecipescome 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.coachesWithoutEmailcounts sub-coaches that cannot become a login and will be skipped.formsandhistoryare alwaysnull. Only a full export shows them.existingsays what earlier runs already put in the studio, per row.partialistruewhen any asked count isnull.isResyncistruewhen the studio already finished a run.
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.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:
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.