Team evidence · noindex

API contracts, ownership & decisions

Who owns each behavior, and why was it built this way?

How to use this doc

Use this in a blame discussion. Classify the layer first. Then quote the row.

Shared request shape

All HTTP calls share one shape. Host and JWT signing secret come from the selected client in src/config/clients.ts.

FieldValue
MethodPOST
ADNEC hosthttps://registration.xporience.com/v2/scan/api
DMG hosthttps://dmg-reg.xporience.com/v2/scan/api
Xporience hosthttps://reg.xporience.com/v2/scan/api
Terrapinn hosthttps://terrapinn.xporience.com/v2/scan/api
Body{ "operation": "<name>", "EPTOKEN": "<HS256 JWT>" }
HeadersContent-Type: application/json · Accept: application/json · User-Agent: AccessControlApp/1.0 (ReactNative) · Authorization: Bearer <AsyncStorage userToken> (usually empty — see architecture)
Timeout10s default · generateLead scan uses 3s

JWT is signed on the device with the active client’s secretKey via getSecretKeyFromClientId / getActiveSecretKey(). Xporience uses a different secret from ADNEC / DMG / Terrapinn. Do not paste secrets into tickets or this site.

Contract: userLogin

Mobile sends (JWT claims): username, password, device_id

Backend is expected to return

HTTP / body codeMeaning mobile implementedMobile ownsBackend owns
200 + dataSession fields: user_id, event_id, event_name, timezone, excel_security_password, email fields, event_theme_color, silent_mode, …Persist user; null space until selectionAuthentication; payload accuracy
400Show messageDisplayValidation
401Show message (e.g. Invalid Data)DisplayAuth / payload rejection
409Device/session conflict; clear unused userTokenDisplay + stay logged outSession uniqueness
500Sanitized user messageSanitizationServer
Timeout / networkConnection copyDetect Axios codesInfra / device

Mobile does not own: whether the password is correct.

Do not log: username+password JWT payload, raw password, raw secret.

Contract: getSpaceList

Mobile sends (JWT claims): event_id, device_id

ResultMobile ownsBackend owns
200 list of spaces (space_id / space_name, type, assigned_badges, event fields)Render, search, write selected space onto sessionWhich spaces exist for this event/device
Empty / invalid rowsEmpty/error UIData quality

No pagination found in mobile.

Contract: generateLead

Mobile sends (JWT claims): badge_number, event_id, badge_type (Visitor | Conference), visited_date, sync_date, user_id, space_id, device_id

Success body mobile reads: data.sync_status — only 1 counts as valid. Missing field = restricted on Home path.

Status (Home laser)Mobile actionMobile ownsBackend owns
200/201 + sync_status 1Save online ValidPersist + UI successBadge allowed for this space/event
200/201 + sync_status ≠ 1No save, RestrictedUIEligibility
801No save, “Select another Space”UISpace service rule
403No save, RestrictedUIAccess rule
404No save, invalid badgeUIBadge existence
409LogoutClear sessionDevice session revoked
400, 401, 405, 407, 408, 5xx, timeoutSave offline (Home)Queue so gate continuesAvailability / auth / timeout
503Not handled on Home (camera/sync treat 503 as special)—Must confirm actual code

If backend returns 503 and mobile Home path does not recognize it, Home may fall through to “other 5xx → save offline”. Camera may not save. That is a contract bug, not “mobile being random.”

Ownership matrix (behaviors, not endpoints)

BehaviorMobile ownsBackend ownsDevice / Gmail owns
Client picker host + secretYes (clients.ts)Provide correct URL/secret per env—
Request shape, JWT claims, timeoutsYesDocument the expected claims—
HTTP status and message / sync_statusInterpret per matrixProduce them—
Local duplicate-today (Visitor)YesMay also reject duplicates—
Offline queueYesAccept delayed visited_date—
Space eligibility (801/503)DisplayRule—
Excel contentsYes (from local arrays)Not involvedFilesystem
Email deliveryAttach + open composerEmail addresses in login payloadMail app
Zebra scan hardwareIntent wiring—DataWedge profile
“Successfully Checked in” on timeoutYes — documented fallbackDid not store the lead—

Sentence: A 500 with “Saving offline” is mobile doing its job. A 404 “Invalid badge” is backend doing its job. A 200 with no sync_status showing Restricted is the contract (decideSuccessValidity).

Decision log (why this way)

Add a row when a behavior is chosen on purpose. Do not argue undocumented history.

IDDateDecisionWhy (from code / known event needs)OwnerStatus
DEC-001unknownSingle POST URL, operation discriminatorMatches existing registration scan APIBackend+mobileIn code
DEC-002unknownClient-signed EPTOKEN instead of server session bearerEvery request is a self-contained JWT. userToken / interceptor unusedIntent unconfirmedIn code; secret in client
DEC-003unknownOffline queue in Redux persistGate must not stop when Wi-Fi diesMobileIn code
DEC-004unknownSuccess toast for offline saveQueue speed over “honest sync state” in the toastMobileIn code — explain this in incidents
DEC-005unknownLogout keeps badge vaultSame device, multiple staff accounts, don’t wipe scansMobile (HomeScreen success copy)In code
DEC-006unknownVisitor local re-checkin vs Conference always newtype === '1' vs otherConfirm with productIn code
DEC-007unknownSpace change blocked until current-space queue syncsAvoid mixing unsynced leads under the wrong spaceMobileIn code
DEC-008unknownReports from device memory, email via GmailNo report APIMobileIn code
DEC-009unknownExcel/admin actions gated by login excel_security_passwordBackend issues the passwordBackend issues; mobile enforcesIn code
DEC-010unknownHome counts derived from arrays, not totalCountCounters drifted historicallyMobile HomeScreenIn code; other screens still use counters
DEC-011unknownScan upload timeout 3s then offlineFast scan laneMobile SCAN_UPLOAD_TIMEOUT_MSIn code
DEC-012unknownRestricted / invalid not queued on HomeDon’t inflate Excel with known-bad badgesMobile NO_SAVE_CODESIn code
DEC-013unknownTwo scan engines (Home vs camera)Camera screen not refactored onto processBadgeScanNot a decision — a riskSee risks
DEC-0142026-08-19Runtime client picker (ADNEC / DMG first)One APK for multiple backends. Persist selectedClientId. Switch only while logged out. Unsynced queue for the client being left must sync or be wiped. Scans tagged client_id; untagged legacy rows are DMG.MobileIn code
DEC-0152026-09Add Xporience + Terrapinn clientsSame picker model; per-client baseUrl + secretKey in clients.ts. Xporience secret differs from shared ADNEC/DMG/Terrapinn secret.MobileIn code
DEC-0162026-09Fixed User-Agent: AccessControlApp/1.0 (ReactNative)Some hosts (notably reg.xporience.com) returned HTML 403 to default OkHttp UA; custom UA reaches JSON API.MobileIn code

When you add DEC-017+, write the reason in one sentence. “Because backend said so” needs a meeting note or ticket id in the incident log.

Meeting decisions (living)

DateDecisionWhoEvidence
—(add rows here; do not create a 12th folder)

Dependencies that affect ownership

DependencyIf it fails, who looks first
dmg-reg.xporience.comBackend / infra (DMG)
registration.xporience.comBackend / infra (ADNEC)
reg.xporience.comBackend / infra (Xporience)
terrapinn.xporience.comBackend / infra (Terrapinn)
Event Wi-Fi / isInternetReachableDevice + venue network
Google generate_204 ping used by Home useNetworkStatusDevice DNS/firewall (can mark offline while API still works, or the reverse after 10s debounce)
Zebra DataWedgeDevice MDM / dataWedgeConfig.ts
Gmail / react-native-mailDevice mail setup
redux-persist / AsyncStorageMobile persistence

Internal screen, not for operators: environment, app version, API base URL, selected client, user_id, space_id, online flag, last operation, last HTTP status, last message, last sync_status, duration, timestamp, copy/export.

Never log: passwords, EPTOKEN, Authorization, excel password, JWT secrets, full personal fields beyond badge number if policy forbids it.

Until that screen exists: screenshot FlashMessage + Charles status line, then file an incident.