App Service (BFF)

One backend-for-frontend becomes the only thing the new app talks to. It owns login, config, hydrated farmer data, idempotent batch sync, files, push and telemetry, and hides the 12 services, 40 datasets and query syntax the phone handles today. Build it inside the existing `urvi-app-service`.

Why

TodayWith the App Service
The app calls about 12 services and about 40 report datasets, and builds dataset params and SQL-like criteria= strings itselfThe app calls one versioned API; dataset names and query syntax stay on the server
A 1,500-farmer setup costs about 300 calls; Home fires about 15 on every openOne config call (often a 304) and a few hydrated snapshot pages
Creating a farmer offline means 7+ queued calls ordered by a client dependency graph and 40 id-back-fill handlersOne batch of mutations keyed by client UUIDs; the server resolves references
Each new season needs an app release (AAP2025_26, …2425)season=current; the server maps it
The Bridge session and the user's password live on the phoneShort-lived tokens only; the Bridge session stays server-side
Push is sent from the phone with a leaked legacy key (and is dead)Server-side, targeted push
Telemetry ships raw request bodies with PIICounters and error codes only

Where to build it. /gateway/urvi-app-service/api/v1/ already serves device sessions and remote app actions to Urvi, so it already has the tenant, auth and deployment plumbing. Add the BFF there as /gateway/urvi-app-service/api/v2/. Paths below are relative to that base.

Responsibilities

ResponsibilityWhat the App Service does
Session bootstrapOne call after login returns user, tenant, roles, flow type (cadre, unit), typed permissions, work-node tree, device status, pending remote actions, minimum and recommended app versions
Config bundle with ETagOne typed bundle: feature switches, season, paging and retry knobs, practices, crop and product catalogue, asset categories, UOM, roles, gender and other dropdowns, AAP reject reasons, help videos, program instructions, form schemas and validation limits. If-None-Match returns 304
AggregationJoins farmer, roles, land, portfolio, farming models, AAP, inventory and attachment URLs on the server instead of the phone
Farmer snapshot + delta syncCursor-paged hydrated snapshot per work node; then changes?cursor= with upserts and tombstones
Idempotent batch syncAccepts ordered mutations keyed by client UUIDs and an idempotency key per op; returns per-op results and a client-to-server id map
File uploadsAccepts photos, KML, signatures and abstracts before their parent exists; returns a fileId that mutations reference
Server-side validationApplies the same rules as the client (land limits, category gates, PMDS ranges, A Grade crop counts, compact block ≥5 acres, ATM C5 ≤0.5 acre, KAP ceilings) and returns typed error codes. Rule parameters are published in the config bundle so both sides use the same numbers
Push fan-outStores device push tokens; sends FCM HTTP v1 messages to the right users (VO's Unit In-charge, the farmer's cadre) with ids only, no names
Telemetry without PIIAccepts screen events and sync-health counters; drops anything resembling names, phone numbers, Aadhaar or request bodies
Tenant routingResolves login@@tenant to the right backend instance (for example the federated ap.gov host) so the app has one URL per environment
Device bindingBinds refresh tokens to an app-scoped device id; enforces the one-device rule server-side when the tenant enables it
App version gatingReads X-App-Version; returns 426 with the minimum version when the app is too old; replaces the "versionName_versionCode" lists in config
Remote actionsDelivers logout and wipe commands and records completion
Season mappingExposes "current season" plans and abstracts; maps them to year-specific backend datasets and tables

What moves out of the app

Logic in the app todayMoves to
Dataset names, KEY=value--KEY2=value params, criteria= strings, ctld/ptld/rtld prefixes, 206-as-more-pagesApp Service adapters
Chunking IE ids by 30/50/100 and resuming pages in prefsSnapshot cursor
Client dependency graph (sync_dependency) and about 40 response handlers that back-fill server idsBatch sync with client UUID resolution
"Get-or-create" of the server IE when a farmer is openedGET /farmers/{id}
Online land availability (PlusOneGetOwnedLandByIEID minus PlusOneGetATMNFUsedAcreageByIEID)Farmer metrics; server-side land-usage check on every mutation
Endorsement pre-checks (category frozen, KAP submitted, all SHGs endorsed)Server-side endorsement state machine (client keeps a preview)
Abstract lookup with IEID then UUID fallback, per yearGET /farmers/{id}/abstract?season=
Bridge login, device registration, session refresh with stored passwordAuth endpoints
Federation host switch (ap.gov → federated host)Tenant routing
Client FCM sendsPush fan-out
Firestore sync logs, Firebase Storage image fallback, raw queue uploadsTelemetry and file endpoints
Season re-labelling of PLIs at rolloverServer; client receives updated rows in deltas
versionName_versionCode allow-lists for Crop Actual and harvestVersion gating and typed flags

Endpoint catalogue

All paths are relative to /gateway/urvi-app-service/api/v2. All requests carry the standard headers listed under Auth.

Auth, session and device

MethodPathReplacesNotes
POST/auth/loginaccount-lib login: PUT /ctld/api/device/deviceid/v1 + Bridge session createBody {login, tenant, password, deviceId}; returns access and refresh tokens. Password never stored on the phone
POST/auth/refreshSilent re-login with stored password; AuthInterceptor retriesRotating refresh token bound to device
POST/auth/logoutPATCH urvi-app-service/v1/user-device-sessions/users/{userId}Revokes tokens, expires binding if configured, removes push token
GET/session/bootstrapDS:PlusOneTenantUserData, DS:PlusOneUrviGeoDataForVOs, device-session check and register, remote actionsOne call after login and on each cold start (cached)
PUT/devices/{deviceId}POST urvi-app-service/v1/user-device-sessionRegisters device details and push token
POST/devices/{deviceId}/binding/takeoverPATCH …/user-device-sessions/users/{userId}?remarks=BINDING_REMOVEDOnly when the tenant allows "Login here"
GET/devices/{deviceId}/actionsGET urvi-app-service/v1/remote-app-actions/actorAlso included in bootstrap
POST/devices/{deviceId}/actions/{actionId}/completePATCH …/remote-app-actions/{id}/execution (with DB zip)Optional encrypted diagnostics fileId, never a raw DB

Config and reference data

MethodPathReplacesNotes
GET/configref-data/class/CLASS.PORTFOLIO_APP_CONFIG (paged, also hourly), DS:PlusOnePortfolioAppConfig, DS:AAP_REJECT_REASON, DS:UrviYouTube, DS:PlusOneInstructions, practices/active, crops-list.json, S3 ref_data.json and classification.json, ProductAPI.getProduct, bundled asset files (gender, roles, UOM), Firebase Remote Config flagsETag; typed sections; includes form schemas and validation limits
GET/geo/children?level=&parent=7 geography datasets (PlusOneGetAllDistricts … PlusOneHabitationSHGList) in 3 duplicate clientsCached server-side

Farmers (read)

MethodPathReplacesNotes
GET/farmers/snapshot?scope=&cursor=&pageSize=PlusOneWorkNodeFarmerListGeoWise[Count][Unit], worknodeIeTagGet, ierolemap by chunks, assets/ownerEntities, asset/attachment/bulk, portfolios/ownerEntities, portfoliosForLineItemIds, farming-model/ownerEntities, farming-model/attachment/bulk, inventory products, AAP2025_26, PLUS_ONE_FARMING_CATEGORY_LAST_UPDATE_DATE, DisabledAssetCodeForCadreOne page of fully hydrated farmers; attachment URLs, not bytes; returns a delta cursor at the end
GET/farmers/changes?cursor=Full re-download on "load more" and per-farmer refreshUpserts and tombstones since the cursor
GET/farmers/{id}?include=assets,portfolio,models,plan,metricsOptionViewModel per-farmer refresh (6-10 calls); POST to worknodeietag/urvi on openServer does get-or-create
GET/farmers/{id}/metricsPlusOneGetOwnedLandByIEID, PlusOneGetATMNFUsedAcreageByIEID, IsKAP202324Owned, leased, used and available acres
GET/farmers/search?geo=&q=PlusOneWorkNodeFarmerListGeoWiseNoPagination, …HusbandFatherSHGWise, …VOWiseNonSHGUsed for lessor lookup and the FCCC filter; never wipes local data
GET/farmers/{id}/plan?season=currentGET asset-service/annual-action-plan-extended, DS:AAP2025_26Includes last season's endorsed values
GET/farmers/{id}/abstract?season=PlusOneSHGAbstractData2024Feb-Admin-IEID, PlusOneMasterFarmerIEUUID[2425], PlusOneMasterFarmerIEID2425Server handles fallbacks

Sync (all writes)

MethodPathReplacesNotes
POST/sync/batchEvery queued write: ptld …/worknodeietag/urvi[/ie] POST/PATCH, ctld/api/ie/v1/iconpic, ctld/api/ierolemap/v1, ctld …/inventoryproduct, asset-service/assets POST/PATCH/DELETE, asset/attachment, portfolios/line-items/v2, portfolioLineItem/v2/crop-activity, portfolioLineItem DELETE, line-items/attachment/item/custom, farming-model (+ attachment, harvest), portfolios/harvest, compact-block POST/PUT/DELETE and member DELETE, endorsement/bulk/save-all, endorsements/attachment, endorsementComment, annual-action-plan-extended, assessment responsesUp to 100 ops per batch; see protocol below
POST/filesMultipart uploads tied to server ids; Firebase Storage fallback; re-download and re-upload of server imagesResumable (Upload-Offset) or presigned; returns fileId; client may upload before the parent exists
GET/sync/batches/{batchId}noneStatus for long-running batches (202 Accepted)

Endorsement and unit work

MethodPathReplacesNotes
GET/endorsements?scope=me|vo&voId=&cursor=endorsement/me (15-min worker), DS:PlusOneEndorsementVOHierarchyStatuses for VO, SHG and farmer in one shape
GET/endorsements/{entityType}/{entityId}/comments?cursor=GET endorsementComment
GET/endorsements/{entityType}/{entityId}/eligibilityClient pre-checks (category frozen, KAP submitted, all SHGs endorsed)Same rule the batch applies; lets the UI explain why Endorse is blocked
GET/vo/{voId}/abstract, /shg/{shgId}/abstractraw-data-service/report/ryss-abstract/{id}/vo|shg, endorsements/attachment downloadReturns a file URL
GET/compact-blocks?voIds=&cursor=compact-block?direction=desc per VO, compact-block/filterIncludes members and computed acres; no hard-coded date cut-off
GET/compact-blocks/eligible-tiles?voIds=&shgIds=Local tile filtering plus line-items/ownerEntities/latest (POST used as read)Server applies the eligibility rules (season, geofence, survey and khata, not in another block)

Analytics, history, notifications, telemetry

MethodPathReplacesNotes
GET/analytics/dashboard?scope=cadre|unit&filters=PlusOneCadreAnalytics, …IEIDCategoryList2, …9Principles, …VOWiseSum, …SHGWiseSum, …9PrinciplesVOWise, …VOWise, PlusOneUnitInchargeInfoOne response per scope
GET/me/login-historyDS:PlusOneCadreLoginHistory
GET/notifications?since=endorsementComment/postedby (hourly worker), topic broadcastsBacked by push
POST/telemetrysync-service/data/so (raw queue every 6 h), sync-service/data/clickstream, Firestore SYNC_LOG, PLI_IMAGE, URVI_ATTACHMENT_ISSUESchema-checked; PII rejected

Surveys (if the assessment library is kept)

MethodPathReplacesNotes
GET/assessments?type=&actor=assessment-service assessmentInstanceAssesses via the librarySurvey definitions in the config bundle where possible
(batch)/sync/batch op assessment.submitAIA default responses and attachment uploadsSignatures and photos as fileId

Sync protocol

Outbox on the device

ColumnPurpose
opId (UUID)Idempotency key, sent as Idempotency-Key per op
entityType, entityLocalId (UUID)What the op touches; local ids are the client UUIDs the app already generates (aliasId, appId, uuid)
kindcreate, update, delete or a named action (endorse, harvest, assessment.submit)
payloadJSON of the changed fields (updates send a patch, not a full DTO)
baseVersionServer version the edit was based on; null for creates
dependsOnOther opIds that must succeed first (for example a file upload before the crop block that references it)
status, attempts, nextAttemptAt, lastErrorDrives backoff and the Pending Sync screen; failed is visible and retryable, never silently dropped
createdAt, userIdOrdering and per-user scoping; logout never deletes another user's ops

Writes to the data table and the outbox happen in one database transaction. A newer edit to the same entity merges into a not-yet-sent update op instead of adding another.

Batch request and response

  1. The worker takes up to 100 ready ops in createdAt order (files first) and sends them as one POST /sync/batch with a batchId.
  2. The server processes ops in order. It resolves every client UUID reference (farmer, land, crop block, compact block) through a per-tenant map of client UUID to server id, so a crop block can reference land created earlier in the same batch.
  3. Each op gets one result:
ResultMeaningClient action
appliedDone; includes serverId and new versionStore ids and version; mark done
duplicateSame idempotency key seen before; returns the original resultSame as applied
conflictbaseVersion is stale; returns the current server entityApply the entity's conflict policy
rejectedValidation failed; typed code and field errorsMark failed, show the reason, let the user fix and resubmit
blockedA dependency in this batch failedKeep pending behind its parent
retryTransient server errorBackoff and resend later
  1. Idempotency keys are kept for at least 30 days, longer than any realistic offline period, so re-sending after a timeout never duplicates.

Conflict policy per entity

EntityPolicy
Farmer profileField-level merge. Server wins on locked fields (frozen category, life status set by admin); client wins on other fields if unchanged on the server since baseVersion
Land assetRejected if the server has authenticated or disabled the parcel since baseVersion; acreage changes re-run the land-usage check
Crop Actual lineVersion check; if the tile is in a compact block, reject with PART_OF_COMPACT_BLOCK and send the server row
ATM, DKGATM: version check. DKG: one answer per farmer per month, last write wins
Harvest, activity, assessment submit, commentsAppend-only, idempotent creates; no conflicts
Compact blockServer-authoritative membership; a tile already in another active block is rejected
EndorsementServer state machine decides allowed transitions; stale transitions are rejected with the current status
Annual planRejected once the farmer, SHG or VO is endorsed

Down-sync and versioning

  • Every entity has a monotonically increasing version and updatedAt on the server.
  • snapshot ends with a cursor; changes?cursor= returns upserts and tombstones after it, scoped to the user's work nodes.
  • The client never overwrites a row that has a pending outbox op; it stores the server row and re-applies the pending patch (or flags a conflict).
  • API versioning: path version (/v2), additive changes only within a version, and support for the current and two previous app releases.

Auth

TopicDesign
ProtocolOAuth 2.0 / OIDC. Preferred: Authorization Code + PKCE through an IdP (Keycloak or similar) fronted by the App Service. Acceptable first step: POST /auth/login with the familiar login, tenant and password form, run server-side against Bridge
TokensAccess token about 15 minutes (JWT with user, tenant, roles, device); refresh token about 30 days, rotating, bound to the device id
StorageRefresh token in Keystore-backed encrypted storage (Compose) or flutter_secure_storage. No password on the device, no AccountManager
RefreshOn 401, refresh once; on refresh failure show "Session expired, log in". Pending outbox ops stay intact and resume after login
OfflineThe app works offline with an expired access token; only sync waits
Login rules keptLower-case and trim; accepted shapes email, login@@tenant, numeric IE login; online required for first login
HeadersAuthorization: Bearer, X-App-Version, X-Device-Id (app-scoped UUID, not ANDROID_ID), X-Tenant, X-Request-Id, Idempotency-Key per op inside batches
App attestationOptional Play Integrity check at login instead of an APP_KEY in the APK

Non-functional requirements

AreaRequirement
Latencyp95 under 1 s for bootstrap and config (cached), under 2 s per snapshot page, under 3 s for a 100-op batch
Payloadgzip or brotli; a snapshot page under about 1 MB compressed (start at 100-200 farmers per page and tune); images by URL, never inline
Low bandwidthWorks on 2G/3G: resumable uploads, small pages, ETag on config, no chatty polling
Availability99.5% monthly, with field working hours (roughly 06:00-21:00 IST) as the protected window; offline mode covers outages
ScaleSize from real tenant counts (users, farmers per VO, ops per day), which the owner must supply; none are in the code
IdempotencyKeys retained 30+ days; batches safe to resend
SecurityTLS only, tenant isolation on every query, audit log of mutations (who, device, app version, time), PII never in logs
PrivacyAadhaar masked in responses unless needed; data stays in India (to confirm with the programme)
ObservabilityTrace per request id; dashboards for sync success rate, conflict and rejection rates by code, oldest pending op per device
CompatibilityCurrent and two previous app versions supported; 426 with minimum version beyond that
Rate limitsPer device and per user, generous for batch sync after long offline periods
TestingContract tests shared with the app; replay tests using anonymised real batches

Architecture

                         +------------------------------+
                         |   Urvi app (Android)         |
                         |   UI -> domain -> repos      |
                         |   local DB (encrypted)       |
                         |   outbox + sync worker       |
                         +--------------+---------------+
                                        | HTTPS, one base URL per env
                                        | Bearer token, X-App-Version,
                                        | X-Device-Id, Idempotency-Key
                                        v
+-----------+          +----------------------------------------------+
| IdP /     |<-------->|  App Service (urvi-app-service /api/v2)      |
| Bridge    |  login,  |                                              |
| auth      |  refresh |  auth | bootstrap | config(ETag) | snapshot  |
+-----------+          |  changes | sync/batch | files | push | telem |
                       |                                              |
                       |  validation rules | id map | idempotency     |
                       |  tenant router | version gate | audit log    |
                       +---+-------+-------+-------+-------+-------+--+
                           |       |       |       |       |       |
                           v       v       v       v       v       v
                       +------+ +------+ +------+ +------+ +-----+ +------+
                       |ptld/ | |asset | |portf-| |report| |assm-| |object|
                       |ctld  | |svc   | |olio  | |data- | |ent  | |store |
                       |(IE,  | |(land,| |svc   | |sets  | |svc  | |(files|
                       |roles,| |endor-| |(crops| |(rtld,| |     | | KML) |
                       |inv.) | |se,AAP| |ATM,CB| |commons|     | |      |
                       +------+ +------+ +------+ +------+ +-----+ +------+
                                        |
                                        v
                               +------------------+
                               | FCM HTTP v1      |----> devices (ids only)
                               +------------------+