App API Protocol — Infra & App Teams
Updated: 2026-09-17. Contract checked against the current repository implementation.
Scope: App-facing startup APIs and Infra access requirements. The App uses one prod System Admin entry by default and a dev System Admin entry when developer mode is enabled. Staging is a server deployment environment; it is not a third App entry.
Status: Native v2, Bundle v2, and Backend Discovery v1 APIs are implemented in the repository. The System Admin contract no longer has a production/beta channel dimension. Bundle candidates are discovered from version folders containing bundle.zip; manifest.json is optional and ignored.
1. Architecture and startup order
The Capacitor App has two independent version identifiers:
- Native: the iOS/Android Store package's increasing build integer. Sysadmin publishes a minimum supported build per platform.
- Bundle: the running JS/assets package's immutable version identifier. Sysadmin publishes the selected shared ZIP and its download URL.
All organizations use the standard publication for the requested appId and platform. There is no organization-specific Bundle selection in this contract.
On each native launch the App must first confirm/apply any Capacitor Updater pending Bundle, then proceed to the remote checks. The full order is: pending Bundle → Native minimum → Bundle resolve → hospital backend discovery → login.
The App connects directly to the published download URL and selected business backend. Sysadmin does not proxy these requests. The App team owns updater integration, application/restart timing, readiness confirmation, recovery, and Store navigation.
2. Base URL and public access
The App team stores the prod and dev System Admin base URLs and switches between them with developer mode. The server's process NODE_ENV controls URL transport policy:
NODE_ENV | App backend / Native update / Bundle download |
|---|---|
production | HTTPS only |
development, test, or staging | HTTP or HTTPS |
The URL policy follows the process mode even when the deployment is described as dev or staging. HTTP remains subject to public-DNS, no-credentials, no-port, no-query, no-fragment, redirect, timeout, and destination-safety checks. The App must separately allow cleartext HTTP when a non-production URL uses it.
Infra exposes POST and OPTIONS for these exact paths:
/api/public/v2/app-versions/check
/api/public/v2/app-bundles/resolve
/api/public/v1/backend-discovery/resolve- Use
Content-Type: application/json; do not send Authorization or cookies. Browser/WebView requests usecredentials: "omit". - Configure the actual Web/WebView origins used by the App. Do not assume the download hostname is the request origin.
- Do not add query parameters or trailing slashes to these API paths.
- Keep administrator pages and APIs restricted separately.
- Native/Bundle v2 accept only
iosandandroid. Do not sendplatform: "web".
3. Check Native minimum version
POST {baseUrl}/api/public/v2/app-versions/check
Request
{
"appId": "hereby-works",
"platform": "ios",
"nativeReleaseNumber": 110
}| Field | Contract |
|---|---|
appId | Configured app identifier, currently hereby-works |
platform | ios or android |
nativeReleaseNumber | Positive integer, maximum 2147483647; embedded in the installed Native package |
App/CI must agree on a monotonically increasing Native integer for each platform. This is not the display version, Git SHA, or Bundle version.
Response — 200
{
"result": "allowed",
"publicationRevision": 8,
"minimumNativeReleaseNumber": 110
}update_required blocks normal use and directs the user to update the Store package. allowed continues to Bundle resolve. A missing policy is 404 NATIVE_POLICY_NOT_FOUND; it must not be treated as allowed.
4. Resolve the published Bundle
POST {baseUrl}/api/public/v2/app-bundles/resolve
Request
{
"appId": "hereby-works",
"platform": "ios",
"nativeReleaseNumber": 110,
"currentBundleVersion": "1.2.2"
}currentBundleVersion is required but nullable: send the identifier embedded in the Bundle actually running, or null if unavailable. Do not send a downloaded-but-not-applied version.
Bundle versions are strings of 1–64 characters matching ^[A-Za-z0-9][A-Za-z0-9._+-]*$. Release tags such as 1.2.3 and short Git SHAs such as 332dc10 are accepted.
Response — update available, 200
{
"result": "update_available",
"publicationRevision": 8,
"minimumNativeReleaseNumber": 110,
"bundle": {
"version": "332dc10",
"downloadUrl": "https://mobile.dev.hereby.me/releases/332dc10/bundle.zip",
"checksumAlgorithm": "sha256",
"checksum": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"sizeBytes": 12000000,
"releaseNotes": "안정성을 개선했어요."
}
}up_to_date includes the published Bundle object and requires no download. native_update_required includes bundle: null and starts the Native Store flow. A different identifier or null produces update_available; the service compares identifiers by equality rather than SemVer ordering.
The server returns SHA-256 hex and byte size for the shared ZIP. The App must verify the artifact, apply it with Capacitor Updater, restart, and confirm readiness. A missing target is 404 BUNDLE_POLICY_NOT_FOUND; it is not up_to_date.
The App calls notifyAppReady immediately on every native launch so Capacitor Updater can confirm that JavaScript loaded. The running Bundle version sent to this endpoint always comes from CapacitorUpdater.current(), never from local storage. After a successful set(), the updater reloads the App into that Bundle; the next launch only confirms current() and does not reload again.
Public lookup returns the published snapshot, not the newest object currently in S3. Uploading a ZIP alone does not change App responses.
5. Resolve the hospital business backend
POST {baseUrl}/api/public/v1/backend-discovery/resolve
Request
{ "code": "OKBEST" }The hospital code is trimmed and normalized to uppercase: 3–32 letters, digits, or hyphens.
Response — 200, illustrative
{
"code": "OKBEST",
"displayName": "테스트 병원",
"endpointId": "11111111-1111-4111-8111-111111111111",
"backendInstanceId": "approved-instance-id",
"apiBaseUrl": "https://api.customer.example",
"mappingVersion": 1,
"publicationRevision": 1
}Confirm the hospital name with the user, then set the business SDK base URL to apiBaseUrl. Do not append /trpc; the SDK handles it. Never send a token saved for another backend to this target.
6. Errors and failure handling
Application errors use:
{
"code": "BUNDLE_POLICY_NOT_FOUND",
"message": "게시된 Bundle 정책이 없어요.",
"requestId": "server-generated-request-id"
}| HTTP status | Meaning |
|---|---|
| 400 | Invalid or missing JSON request; schemas reject unknown fields |
| 403 | Origin or preflight denied |
| 404 | Unknown path or unpublished policy |
| 413 / 415 | Request too large / unsupported content type |
| 429 | Rate limit; respect Retry-After |
| 500 / 503 | Server error / temporary unavailability |
Responses use Cache-Control: no-store; immutable versioned ZIP files may use normal CDN caching. A network or proxy error is neither a mandatory-update result nor proof that the App is up to date. The App team must document retry/offline/fallback behavior before release.
7. Acceptance checklist
Before declaring an environment ready:
- Infra confirms the deployed System Admin base URLs, public routes, CORS, and reachability of the returned download URLs.
- Admin confirms one app-level source and publication with the intended iOS/Android minimums and shared
bundle.zipcandidate. - App confirms the Native integer mapping, running Bundle identifier, and verification of returned checksum and size.
- App tests Native blocking, unchanged Bundle, different Bundle, immediate application/restart, readiness confirmation, failure recovery, environment switching, and hospital discovery/login on real iOS and Android devices.
- Record unresolved acceptance gaps explicitly. A successful Docker build alone is not end-to-end acceptance.