Onboarding a User
Adding a user sounds like inserting a row. It isn't. A usable ALERTCalifornia account has to exist in Entra ID (the identity), in SQL (agency and camera relationships), and in Cosmos (preferences), possibly belong to Entra groups, and receive a welcome email with a temporary password. Miss one and the account is half-broken. The Admin API treats onboarding as the multi-store operation it really is.
The one idea to hold on to
Onboarding is one idempotent service call that writes every store, and bulk onboarding is
offloaded to a queue so the request never blocks on thousands of identity operations.
UserLifecycleService.OnboardUserAsync is the single path — interactive and bulk both go
through it:
It either creates a new Entra user (with a generated temporary password) or verifies an existing one by object id — with retry for Graph replication lag, because a just-created user isn't instantly readable. Then it syncs SQL, inserts the Cosmos document, assigns groups if asked, and emails new users their temporary password.
Because onboarding touches five systems, a partial failure (Graph succeeded, SQL timed out) is
normal, and the response says so: 200 when every step succeeded, 207 MultiStatus when
some did and some didn't, with per-store outcomes in steps. The fix for a partial onboard is
to re-run the same onboard — each step is safe to repeat — not to hand-patch the store that
failed. Reaching into SQL or Cosmos to "finish" a half-onboard invites drift.
Why bulk onboarding goes through a queue
Onboarding one user is a handful of Graph calls. Onboarding a CSV of a thousand is thousands —
far too long to hold an HTTP request open. So UserIngestionController doesn't do the work
inline. It persists the batch and fans it out:
UserIngestionService.CreateBatchAsyncwrites aningestionBatchrollup plus oneingestionRowdocument per row to the CosmosuserOnboardingOpscontainer, and enqueues one message per row on theuser-ingestionqueue.- The in-process
UserIngestionQueueProcessorbackground service drains that queue, runningOnboardUserAsyncper row and patching the batch counters as rows complete. - The caller gets a batch id back immediately and polls
GET /UserIngestion/batches/{batchId}for progress.
The batch status walks Pending → Processing → Completed (or CompletedWithErrors), with live
succeeded/failed/skipped counts — so a large import is observable while it runs, not a
black box that either returns after ten minutes or times out.
Wrapping up
A user account spans Entra, SQL, Cosmos, groups, and email, so onboarding is one idempotent
UserLifecycleService operation that reports per-store outcomes and is meant to be retried,
not repaired. Bulk imports run through the user-ingestion queue and an in-process background
processor that updates batch counters, so the request returns instantly and progress is
pollable. Offboard, password reset, and bulk reset round out the same service.
Next, pushing the platform's data outward to the map: Syncing to the Map.