8.5 KiB
Audit: Migrating from userId to workspaceId
This document audits every place the project uses userId (and user_id) so the
migration to a workspaceId-scoped model can be done completely. There are
currently no references to workspaceId anywhere in the codebase — this is a
greenfield rename/replacement.
How scoping currently works (the mental model)
- Source of truth set at login —
login_controller.dart:70storesprefs.setString('userId', response['uuid']). The backend'suuidbecomes the locally cacheduserId. - Anonymous fallback —
home_screen.dart:127,162generate a randomUuid().v4()and store it asuserIdwhen none exists (anonymous/guest flow). - Consumption — Almost everywhere else reads
prefs.getString("userId")and passes it to the backend as:- a query param (
?userId=...or_queryParams['userId']), - a request body field (
BatchActionRequest.userId,FormData.userId), - a model field deserialized from
json['userId'].
- a query param (
Migration decision needed (see "Open questions"): is
workspaceIda NEW value the backend returns at login, or simply a rename of the existinguuid? And will the API now accept/scope by a header (e.g.X-Workspace-Id) instead of a query param? The plan below assumes a rename + the option to centralize scoping.
Inventory of changes required
A. Persistence / key name (the keystone)
The SharedPreferences key string "userId" is the single most important thing to
change consistently. If the key string is changed in one place but not another,
reads will silently return null.
| File | Line | Current | Action |
|---|---|---|---|
lib/screens/onboarding/login/login_controller.dart |
70 | prefs.setString('userId', response['uuid']) |
Write workspaceId. Confirm backend field — may become response['workspaceId']. |
lib/screens/home/home_screen.dart |
127, 162 | prefs.setString("userId", Uuid().v4()) |
Rename key to workspaceId (anonymous fallback). |
lib/screens/home/home_screen.dart |
126, 130, 151, 152, 161, 165 | prefs.getString("userId") |
Rename key reads to workspaceId. |
lib/screens/onboarding/phone/phone_controller.dart |
48 | "tempUid": prefs.get("userId") |
Rename key read; confirm whether tempUid body field name should change too. |
Recommendation: introduce a single constant, e.g.
const kWorkspaceIdKey = 'workspaceId';and a small helper (Prefs.workspaceId) so the key string is never duplicated again. A migration shim should also copy any existinguserIdvalue toworkspaceIdon first launch so logged-in users aren't logged out.
B. Query-param / URL usages (sent to backend)
| File | Line | Current |
|---|---|---|
lib/screens/recipient/recipients_screen.dart |
69, 271 | _queryParams['userId'] = prefs.getString("userId")! |
lib/screens/history/history_controller.dart |
119, 146 | 'userId': userId (query map) |
lib/screens/groups/controllers/group_controller.dart |
72, 197, 233, 269 | 'userId': userId and ?userId=$userId in URLs |
lib/screens/groups/controllers/batch_controller.dart |
369, 528, 584 | ?userId=$userId in URLs |
lib/screens/home/home_controller.dart |
158 | ...&userId=$userId in URL |
Action: rename the param key to workspaceId (or move scoping into a header — see C).
C. HTTP layer (lib/http/http.dart)
Currently the HTTP wrapper only adds Authorization: Bearer <token> headers
(getHeaders() around lines 28–36). It does not know about userId.
Action / opportunity: if the backend now scopes by a workspace header, this is the
single best place to inject X-Workspace-Id once, eliminating the dozens of
per-call query params. Otherwise, leave the per-call params and just rename them.
D. Request-body model fields & method params (Dart-side, hand-written)
These are the hand-written declarations and call sites to rename
(userId -> workspaceId):
lib/screens/groups/models/group_batch_model.dart— fields at lines 9, 90, 117, 134 and constructors 26, 100, 121, 141 (includesBatchActionRequest.userId).lib/screens/groups/models/recipient_group_model.dart— fields 11, 33, 58 and constructors 18, 42, 64.lib/screens/groups/models/batch_item_update_model.dart— field 38, constructor 40.lib/screens/transactions/transaction_model.dart— field 53, constructor 101.lib/screens/transaction_controller.dart— defaults/params 41, 71, 152 (the freezedFormData.userId).lib/screens/groups/controllers/group_controller.dart— method params 56, 118, 123, 193, 229, 244, 257 and internal calls 120, 128, 251.lib/screens/groups/controllers/batch_controller.dart— method params 363, 466, 494, 522, 545, 581, 598 andBatchActionRequest(userId: ...)at 473, 501, 552; recursive call 606.lib/screens/history/history_controller.dart— method params 110, 141.lib/screens/home/home_controller.dart— method param 151.
E. Call sites in screens (read prefs -> pass through)
lib/screens/pay/pay_controller.dart— 101 (userId: prefs.getString("userId") ?? ''into FormData).lib/screens/groups/screens/group_create_screen.dart— 84, 91.lib/screens/groups/screens/group_detail_screen.dart— 555, 678, 703 (widget.group.userId).lib/screens/groups/screens/batch_detail_screen.dart— local_userIdat 36, 88 and uses at 90, 139, 155, 434, 487, 675, 688, 1438.lib/screens/groups/screens/create_group_from_file_screen.dart— 68, 72.lib/screens/groups/screens/groups_screen.dart— 33, 39, 40, 44, 46, 52, 75.lib/screens/groups/screens/batch_create_screen.dart— 92, 96, 118, 133, 157.lib/screens/history/history_screen.dart— 67, 74, 230, 330, 354, 499, 512.
F. Generated files (DO NOT hand-edit — regenerate)
These contain userId derived from @JsonKey/freezed annotations in the
hand-written sources above. After renaming the source fields (and the JSON keys),
regenerate with build_runner:
lib/screens/transaction_controller.g.dartlib/screens/transaction_controller.freezed.dartlib/screens/groups/models/batch_item_update_model.g.dartlib/screens/groups/models/recipient_group_model.g.dartlib/screens/groups/models/group_batch_model.g.dartlib/screens/transactions/transaction_model.g.dart
Regenerate command:
dart run build_runner build --delete-conflicting-outputs
JSON wire-key decision (critical)
The generated serializers currently read/write the JSON key 'userId'
(e.g. json['userId'], 'userId': instance.userId). You must decide whether the
backend JSON contract also changes to workspaceId:
- If the API now expects/returns
workspaceId: rename the Dart field AND let the serializer use the new key (or add@JsonKey(name: 'workspaceId')). - If the API still uses
userIdon the wire but you only want the Dart field renamed: keep the wire key with@JsonKey(name: 'userId')on the renamed field.
This must be coordinated with the backend team before regenerating.
Recommended migration order
- Confirm backend contract (see Open questions) — header vs query param, and the JSON key name.
- Add a
kWorkspaceIdKeyconstant + prefs helper, plus a one-time migration shim that copies existinguserId->workspaceId. - (Optional but recommended) Inject
X-Workspace-Idcentrally inhttp.dart. - Rename hand-written model fields / method params (sections D & E).
- Update query-param/URL usages (section B), or remove them if moved to header.
- Update the prefs key reads/writes (section A).
- Regenerate build_runner outputs (section F).
flutter analyzeand test the anonymous flow, login flow, history, groups, batches, recipients, and pay flows end-to-end.
Open questions for the team
- Is
workspaceIda brand-new value from the backend (e.g.response['workspaceId']), or just a rename of the existinguuid/userId? - Should scoping move to a request header (
X-Workspace-Id) so it can be set once inhttp.dart, instead of ~15 per-call query params/body fields? - Does the JSON wire contract change to
workspaceId, or stayuserId? - For the anonymous/guest flow (
home_screen.dart), is a client-generatedUuid().v4()still acceptable as aworkspaceId, or must the backend mint it? - Does
phone_controller'stempUidbody key need renaming too?
Summary counts
- Non-generated
.dartfiles to edit: 21 - Generated files to regenerate: 6
- Total non-generated
userId/user_idoccurrences: ~112 - Existing
workspaceIdreferences: 0