Skip to main content

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.

Gotcha: every step is idempotent on purpose — retry, don't repair

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:

  1. UserIngestionService.CreateBatchAsync writes an ingestionBatch rollup plus one ingestionRow document per row to the Cosmos userOnboardingOps container, and enqueues one message per row on the user-ingestion queue.
  2. The in-process UserIngestionQueueProcessor background service drains that queue, running OnboardUserAsync per row and patching the batch counters as rows complete.
  3. 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.