App Version — client update gate (iOS / Android / Web)
The whole module reduces to one idea: a single public REST endpoint that the mobile/web clients poll to learn the latest published app version and whether the update is forced. It owns no data of its own — it is a thin read-only projection over the tenant
Configdocument plus two hardcoded store URLs.
Source: BE src/modules/version · Admin — none (values are edited in the Config admin screen, not a dedicated "version" screen)
1. Purpose & scope
Lets the MSGold mobile apps (and the web client) check at startup whether they are running an outdated build and whether they must update before continuing.
- Does: expose
GET /api/versionreturning the current iOS/Android/Web version, aforceUpdateflag per platform, and a storedownloadUrlper platform. - Does NOT: store version history / changelog (there is no
Versioncollection — see §2), authenticate the caller, scope by branch, or push notifications. It is a stateless lookup. The version values live on theConfigdocument and are managed by the Config module, not here.
This module is a NestJS REST controller only — there is no resolver, service, repository, or schema of its own:
src/modules/version/
version.controller.ts ← the single GET handler
version.module.ts ← imports ApConfigModule, registers the controller
version.module.ts (verbatim):
@Module({
imports: [ApConfigModule],
controllers: [VersionController],
})
export class VersionModule {}Registered in app.module.ts (line 114). The app sets no global prefix (main.ts has no setGlobalPrefix), so the live route is exactly /api/version.
2. Data model
This module defines no collection. The data it returns comes from the Config collection (config/config.schema.ts), read via ApConfigService.findLast().
The four version-related fields on Config (config/config.schema.ts lines 12–19):
| Field | Type | Required? | Description |
|---|---|---|---|
androidVersion |
string |
no | Latest published Android version string (e.g. "1.4.2"). |
androidForceUpdate |
boolean |
no | If true, Android clients below androidVersion must update. |
iosVersion |
string |
no | Latest published iOS version string. |
iosForceUpdate |
boolean |
no | If true, iOS clients below iosVersion must update. |
// config/config.schema.ts (trimmed)
@ApSchema({ collection: `configs`, timestamps: true })
export class Config extends BaseSchema {
@Prop({}) androidVersion: string;
@Prop({}) androidForceUpdate: boolean;
@Prop({}) iosVersion: string;
@Prop({}) iosForceUpdate: boolean;
// ...many other tenant settings...
}There is no version/changelog history — Config is a single per-company settings document; updating a version overwrites the previous value. findLast() returns the most recent Config for the company (config/config.service.ts):
override async findLast(query?: Partial<Config>): Promise<Config> {
this.configRepo.ignoreBranchId = true; // config is company-wide, not branch-scoped
return await this.configRepo
.findLast({ ...query, companyId: this.contextSvc?.user?.companyId })
.finally(() => { this.configRepo.ignoreBranchId = false; });
}Tenant note:
findLast()filters bycompanyIdfrom the request context. The/api/versionendpoint has no auth context (see §5), socompanyIdisundefinedandfindLastreturns the most recentConfigdocument across the database. In practice the deployment is effectively single-tenant for this endpoint (the MSGold app).
3. API surface
REST
| Method | Route | Body | Auth | Response |
|---|---|---|---|---|
GET |
/api/version |
— | public (no guard) | JSON: { ios, android, web } |
The handler (version/version.controller.ts, verbatim):
@Controller("api/version")
export class VersionController {
constructor(private readonly configSvc: ApConfigService) {}
@Get("/")
async version(): Promise<any> {
const config = await this.configSvc.findLast();
return {
ios: {
version: config.iosVersion,
forceUpdate: config.iosForceUpdate,
downloadUrl: "https://apps.apple.com/us/app/msgold/id6450125286",
},
android: {
version: config.androidVersion,
forceUpdate: config.androidForceUpdate,
downloadUrl:
"https://play.google.com/store/apps/details?id=com.msgold.app",
},
web: { v: "1.2", forceUpdate: true },
};
}
}Example response shape:
{
"ios": { "version": "1.4.2", "forceUpdate": false, "downloadUrl": "https://apps.apple.com/us/app/msgold/id6450125286" },
"android": { "version": "1.4.2", "forceUpdate": false, "downloadUrl": "https://play.google.com/store/apps/details?id=com.msgold.app" },
"web": { "v": "1.2", "forceUpdate": true }
}No GraphQL surface in this module. The version values are written via the Config module's GraphQL mutation (updateConfig) — see §7.
4. Business rules & calculations
- No server-side comparison. The endpoint reports the latest version + the
forceUpdateflag; it does not receive the client's current version or compute "is outdated". The version comparison (clientVersion < serverVersion) happens on the client. webis hardcoded —{ v: "1.2", forceUpdate: true }is a literal in the controller, not read fromConfig. Bumping the web version requires a code change + redeploy.- Store URLs are hardcoded to the MSGold App Store / Play Store listings.
- No validation, no state machine, no side effects. Pure read. Writing nothing, it emits no GL legs, stock rows, audit entries, or notifications.
There is no transactionality — it is a single findLast read.
5. Permissions
None. VersionController carries no @ApGqlAuthorize, no @UseGuards, and no permission module/action. It is a deliberately public, unauthenticated endpoint so an app can check for a forced update before the user logs in.
Contrast: the Config write path (updateConfig mutation) IS guarded and audited (@AuditMeta({ module: 'config', ... })) — see Config.
6. Flows
6.1 Client startup version check (happy path)
- App launches → client issues
GET /api/version. VersionController.version()callsApConfigService.findLast().findLast()setsignoreBranchId = true, queries theconfigscollection for the latest doc (bycompanyIdif present in context — absent here), returns theConfig.- Controller maps
Config.{ios,android}Version+{ios,android}ForceUpdateinto the response, appends hardcodeddownloadUrls and the literalwebblock. - Client compares its bundled build version against the returned
versionfor its platform:- up to date → continue normally.
- outdated and
forceUpdate === true→ block the app, show a blocking "Update required" screen linking todownloadUrl. - outdated and
forceUpdate === false→ show a dismissible "Update available" prompt.
6.2 Operator publishes a new version (the write side)
- Admin opens the Config screen (
zerp-admin/src/modules/config, not the wingold module) and edits the Android/iOS version + force-update toggles. - Admin saves → GraphQL
updateConfig(UpdateConfigInput)mutation (config/config.resolver.ts), which writes the newandroidVersion/iosVersion/*ForceUpdateontoConfig(audited). - Next time any app calls
GET /api/version, the new values are served.
Unhappy paths
- No
Configdocument exists →findLast()returnsnull/undefined and the controller dereferencesconfig.iosVersion, throwing →500. There is no null guard in the controller. (Documented gotcha — see §9.) - Version fields unset on
Config→ the response carriesversion: null; the client decides what to do with a missing version.
7. Admin UI
There is no dedicated version admin module. The four fields are surfaced inside the general Config / settings screen:
- The
UpdateConfigInput(config/config.dto.ts, lines 138–144 and schema.gql line 11203+) exposesandroidVersion,androidForceUpdate,iosVersion,iosForceUpdate. - They are edited and persisted through the Config context's update method and the
updateConfigmutation.
See Config for the screen, Formik schema, and context methods.
8. Dependencies & integrations
- Depends on:
ApConfigModule/ApConfigService(the only injected dependency) →Configcollection. - Consumed by: the MSGold mobile apps (iOS + Android) and the web client at startup. These are the external clients that poll
/api/version. - External services: none called server-side. The hardcoded
downloadUrls point at the App Store / Play Store listings; the apps open those URLs. - No cron / jobs / events.
9. Gotchas & project-specific rules
- No
Versioncollection / changelog. Despite the module name, nothing here records version history. It reports one current value per platform, stored onConfig. If you need a changelog or staged rollout, that is net-new. - Web version + store URLs are hardcoded in the controller. Changing them needs a code change + redeploy, not a config edit.
- Public + unauthenticated by design — must work pre-login for a forced-update gate. Don't add an auth guard without giving the apps an unauthenticated path.
findLast()is effectively global here — with no request context,companyIdis undefined, so it returns the latestConfigin the DB. This module assumes a single MSGold deployment; in a true multi-tenant setup you'd need to pass a company/app identifier into the endpoint.- No null guard — if no
Configexists, the endpoint 500s. Seed aConfigbefore relying on it. - Force-update logic lives on the client. The server only reports
version+forceUpdate; the "is my build older?" comparison is the app's responsibility.