Why
| Today | With the App Service |
|---|
The app calls about 12 services and about 40 report datasets, and builds dataset params and SQL-like criteria= strings itself | The 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 open | One 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 handlers | One 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 phone | Short-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 PII | Counters 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
| Responsibility | What the App Service does |
|---|
| Session bootstrap | One 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 ETag | One 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 |
| Aggregation | Joins farmer, roles, land, portfolio, farming models, AAP, inventory and attachment URLs on the server instead of the phone |
| Farmer snapshot + delta sync | Cursor-paged hydrated snapshot per work node; then changes?cursor= with upserts and tombstones |
| Idempotent batch sync | Accepts ordered mutations keyed by client UUIDs and an idempotency key per op; returns per-op results and a client-to-server id map |
| File uploads | Accepts photos, KML, signatures and abstracts before their parent exists; returns a fileId that mutations reference |
| Server-side validation | Applies 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-out | Stores 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 PII | Accepts screen events and sync-health counters; drops anything resembling names, phone numbers, Aadhaar or request bodies |
| Tenant routing | Resolves login@@tenant to the right backend instance (for example the federated ap.gov host) so the app has one URL per environment |
| Device binding | Binds refresh tokens to an app-scoped device id; enforces the one-device rule server-side when the tenant enables it |
| App version gating | Reads X-App-Version; returns 426 with the minimum version when the app is too old; replaces the "versionName_versionCode" lists in config |
| Remote actions | Delivers logout and wipe commands and records completion |
| Season mapping | Exposes "current season" plans and abstracts; maps them to year-specific backend datasets and tables |
What moves out of the app
| Logic in the app today | Moves to |
|---|
Dataset names, KEY=value--KEY2=value params, criteria= strings, ctld/ptld/rtld prefixes, 206-as-more-pages | App Service adapters |
| Chunking IE ids by 30/50/100 and resuming pages in prefs | Snapshot cursor |
Client dependency graph (sync_dependency) and about 40 response handlers that back-fill server ids | Batch sync with client UUID resolution |
| "Get-or-create" of the server IE when a farmer is opened | GET /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 year | GET /farmers/{id}/abstract?season= |
| Bridge login, device registration, session refresh with stored password | Auth endpoints |
Federation host switch (ap.gov → federated host) | Tenant routing |
| Client FCM sends | Push fan-out |
| Firestore sync logs, Firebase Storage image fallback, raw queue uploads | Telemetry and file endpoints |
| Season re-labelling of PLIs at rollover | Server; client receives updated rows in deltas |
versionName_versionCode allow-lists for Crop Actual and harvest | Version 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
| Method | Path | Replaces | Notes |
|---|
| POST | /auth/login | account-lib login: PUT /ctld/api/device/deviceid/v1 + Bridge session create | Body {login, tenant, password, deviceId}; returns access and refresh tokens. Password never stored on the phone |
| POST | /auth/refresh | Silent re-login with stored password; AuthInterceptor retries | Rotating refresh token bound to device |
| POST | /auth/logout | PATCH urvi-app-service/v1/user-device-sessions/users/{userId} | Revokes tokens, expires binding if configured, removes push token |
| GET | /session/bootstrap | DS:PlusOneTenantUserData, DS:PlusOneUrviGeoDataForVOs, device-session check and register, remote actions | One call after login and on each cold start (cached) |
| PUT | /devices/{deviceId} | POST urvi-app-service/v1/user-device-session | Registers device details and push token |
| POST | /devices/{deviceId}/binding/takeover | PATCH …/user-device-sessions/users/{userId}?remarks=BINDING_REMOVED | Only when the tenant allows "Login here" |
| GET | /devices/{deviceId}/actions | GET urvi-app-service/v1/remote-app-actions/actor | Also included in bootstrap |
| POST | /devices/{deviceId}/actions/{actionId}/complete | PATCH …/remote-app-actions/{id}/execution (with DB zip) | Optional encrypted diagnostics fileId, never a raw DB |
Config and reference data
| Method | Path | Replaces | Notes |
|---|
| GET | /config | ref-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 flags | ETag; typed sections; includes form schemas and validation limits |
| GET | /geo/children?level=&parent= | 7 geography datasets (PlusOneGetAllDistricts … PlusOneHabitationSHGList) in 3 duplicate clients | Cached server-side |
Farmers (read)
| Method | Path | Replaces | Notes |
|---|
| 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, DisabledAssetCodeForCadre | One 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 refresh | Upserts and tombstones since the cursor |
| GET | /farmers/{id}?include=assets,portfolio,models,plan,metrics | OptionViewModel per-farmer refresh (6-10 calls); POST to worknodeietag/urvi on open | Server does get-or-create |
| GET | /farmers/{id}/metrics | PlusOneGetOwnedLandByIEID, PlusOneGetATMNFUsedAcreageByIEID, IsKAP202324 | Owned, leased, used and available acres |
| GET | /farmers/search?geo=&q= | PlusOneWorkNodeFarmerListGeoWiseNoPagination, …HusbandFatherSHGWise, …VOWiseNonSHG | Used for lessor lookup and the FCCC filter; never wipes local data |
| GET | /farmers/{id}/plan?season=current | GET asset-service/annual-action-plan-extended, DS:AAP2025_26 | Includes last season's endorsed values |
| GET | /farmers/{id}/abstract?season= | PlusOneSHGAbstractData2024Feb-Admin-IEID, PlusOneMasterFarmerIEUUID[2425], PlusOneMasterFarmerIEID2425 | Server handles fallbacks |
Sync (all writes)
| Method | Path | Replaces | Notes |
|---|
| POST | /sync/batch | Every 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 responses | Up to 100 ops per batch; see protocol below |
| POST | /files | Multipart uploads tied to server ids; Firebase Storage fallback; re-download and re-upload of server images | Resumable (Upload-Offset) or presigned; returns fileId; client may upload before the parent exists |
| GET | /sync/batches/{batchId} | none | Status for long-running batches (202 Accepted) |
Endorsement and unit work
| Method | Path | Replaces | Notes |
|---|
| GET | /endorsements?scope=me|vo&voId=&cursor= | endorsement/me (15-min worker), DS:PlusOneEndorsementVOHierarchy | Statuses for VO, SHG and farmer in one shape |
| GET | /endorsements/{entityType}/{entityId}/comments?cursor= | GET endorsementComment | |
| GET | /endorsements/{entityType}/{entityId}/eligibility | Client 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}/abstract | raw-data-service/report/ryss-abstract/{id}/vo|shg, endorsements/attachment download | Returns a file URL |
| GET | /compact-blocks?voIds=&cursor= | compact-block?direction=desc per VO, compact-block/filter | Includes 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
| Method | Path | Replaces | Notes |
|---|
| GET | /analytics/dashboard?scope=cadre|unit&filters= | PlusOneCadreAnalytics, …IEIDCategoryList2, …9Principles, …VOWiseSum, …SHGWiseSum, …9PrinciplesVOWise, …VOWise, PlusOneUnitInchargeInfo | One response per scope |
| GET | /me/login-history | DS:PlusOneCadreLoginHistory | |
| GET | /notifications?since= | endorsementComment/postedby (hourly worker), topic broadcasts | Backed by push |
| POST | /telemetry | sync-service/data/so (raw queue every 6 h), sync-service/data/clickstream, Firestore SYNC_LOG, PLI_IMAGE, URVI_ATTACHMENT_ISSUE | Schema-checked; PII rejected |
Surveys (if the assessment library is kept)
| Method | Path | Replaces | Notes |
|---|
| GET | /assessments?type=&actor= | assessment-service assessmentInstanceAssesses via the library | Survey definitions in the config bundle where possible |
| (batch) | /sync/batch op assessment.submit | AIA default responses and attachment uploads | Signatures and photos as fileId |
Sync protocol
Outbox on the device
| Column | Purpose |
|---|
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) |
kind | create, update, delete or a named action (endorse, harvest, assessment.submit) |
payload | JSON of the changed fields (updates send a patch, not a full DTO) |
baseVersion | Server version the edit was based on; null for creates |
dependsOn | Other opIds that must succeed first (for example a file upload before the crop block that references it) |
status, attempts, nextAttemptAt, lastError | Drives backoff and the Pending Sync screen; failed is visible and retryable, never silently dropped |
createdAt, userId | Ordering 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
- The worker takes up to 100 ready ops in
createdAt order (files first) and sends them as one POST /sync/batch with a batchId.
- 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.
- Each op gets one result:
| Result | Meaning | Client action |
|---|
applied | Done; includes serverId and new version | Store ids and version; mark done |
duplicate | Same idempotency key seen before; returns the original result | Same as applied |
conflict | baseVersion is stale; returns the current server entity | Apply the entity's conflict policy |
rejected | Validation failed; typed code and field errors | Mark failed, show the reason, let the user fix and resubmit |
blocked | A dependency in this batch failed | Keep pending behind its parent |
retry | Transient server error | Backoff and resend later |
- 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
| Entity | Policy |
|---|
| Farmer profile | Field-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 asset | Rejected if the server has authenticated or disabled the parcel since baseVersion; acreage changes re-run the land-usage check |
| Crop Actual line | Version check; if the tile is in a compact block, reject with PART_OF_COMPACT_BLOCK and send the server row |
| ATM, DKG | ATM: version check. DKG: one answer per farmer per month, last write wins |
| Harvest, activity, assessment submit, comments | Append-only, idempotent creates; no conflicts |
| Compact block | Server-authoritative membership; a tile already in another active block is rejected |
| Endorsement | Server state machine decides allowed transitions; stale transitions are rejected with the current status |
| Annual plan | Rejected 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
| Topic | Design |
|---|
| Protocol | OAuth 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 |
| Tokens | Access token about 15 minutes (JWT with user, tenant, roles, device); refresh token about 30 days, rotating, bound to the device id |
| Storage | Refresh token in Keystore-backed encrypted storage (Compose) or flutter_secure_storage. No password on the device, no AccountManager |
| Refresh | On 401, refresh once; on refresh failure show "Session expired, log in". Pending outbox ops stay intact and resume after login |
| Offline | The app works offline with an expired access token; only sync waits |
| Login rules kept | Lower-case and trim; accepted shapes email, login@@tenant, numeric IE login; online required for first login |
| Headers | Authorization: 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 attestation | Optional Play Integrity check at login instead of an APP_KEY in the APK |
Non-functional requirements
| Area | Requirement |
|---|
| Latency | p95 under 1 s for bootstrap and config (cached), under 2 s per snapshot page, under 3 s for a 100-op batch |
| Payload | gzip 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 bandwidth | Works on 2G/3G: resumable uploads, small pages, ETag on config, no chatty polling |
| Availability | 99.5% monthly, with field working hours (roughly 06:00-21:00 IST) as the protected window; offline mode covers outages |
| Scale | Size from real tenant counts (users, farmers per VO, ops per day), which the owner must supply; none are in the code |
| Idempotency | Keys retained 30+ days; batches safe to resend |
| Security | TLS only, tenant isolation on every query, audit log of mutations (who, device, app version, time), PII never in logs |
| Privacy | Aadhaar masked in responses unless needed; data stays in India (to confirm with the programme) |
| Observability | Trace per request id; dashboards for sync success rate, conflict and rejection rates by code, oldest pending op per device |
| Compatibility | Current and two previous app versions supported; 426 with minimum version beyond that |
| Rate limits | Per device and per user, generous for batch sync after long offline periods |
| Testing | Contract 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)
+------------------+