KgwariC4 Architecture
01

Kgwari Clientkgwari-frontend-app · TypeScript · Expo · React Native + Web

Expo 54 expo-router 6 React 19.1 RN 0.81 pnpm workspaces i18next S3 + CloudFront

1A — ContextPeople & external systems

Three kinds of person now use the client, and the difference matters: a member carries a status word, a verified account carries a trust mark, and a reviewer walks the same build with the network swapped out.

fe-context diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title Kgwari Client — System Context

Person(member, "Member", "Discovers, searches, cellars and reviews wine.\nStatus word: enthusiast → collector.")
Person_Ext(verified, "Verified Account", "Professional · producer · distributor.\nTrust mark earned at business onboarding.")
Person_Ext(reviewer, "Designer / Reviewer", "Walks deterministic offline flows via App Doubles.")

System(frontend, "Kgwari Client", "kgwari-frontend-app — universal Expo app (web, iOS, Android).\nBuildFlavor x AppEnvironment runtime split.")

System_Ext(backend, "Kgwari API", "HTTPS / JSON over API Gateway")
System_Ext(cognito, "Amazon Cognito", "Passkey-first sign-in, OTP fallback, JWT issuance")
System_Ext(cdn, "CloudFront + S3", "Static web hosting (dev / uat / beta / production)")
System_Ext(bedrock, "Amazon Bedrock", "Build time only — fills draft locale catalogues\nfor a native speaker to review")
System_Ext(ghpkg, "GitHub Packages", "@edwardseshoka contracts · foundation · samples")

Rel(member, frontend, "Uses", "HTTPS")
Rel(verified, frontend, "Claims records · publishes verdicts", "HTTPS")
Rel(reviewer, frontend, "Validates with App Doubles", "non-release build")
Rel(frontend, cdn, "Loaded from", "HTTPS")
Rel(frontend, backend, "Calls REST API", "HTTPS / JSON")
Rel(frontend, cognito, "Passkey registration & assertion, OTP fallback", "JWT")
Rel_Back(frontend, ghpkg, "Resolves at build time", "npm install")
Rel(frontend, bedrock, "Generates draft catalogues", "pnpm i18n:translate")

@enduml

1B — ContainerTwelve feature domains, one bundle

Thirty-four workspace packages compile to a single Expo bundle. The composition root is the only place a concrete adapter is named, which is what makes the App Doubles swap a configuration change rather than a code change.

fe-container diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title Kgwari Client — Containers (34 workspace packages, one bundle)

Person(member, "Member", "Web / iOS / Android")

System_Boundary(fe, "kgwari-frontend-app") {
  Container(shell, "Expo Router Shell", "expo-router 6 · React 19.1 · RN 0.81", "(app)/(tabs): discover · search · cellar · community · profile\n/wine/[wineId] · /member · /developer-settings")

  Container(composition, "Composition Root", "main/src/composition", "AppComponents + per-feature composition;\nthe only place a concrete adapter is named")

  Container_Boundary(features, "Feature Packages — 12 domains, 27 packages (workspace:*)") {
    Container(memberF, "member-domain / -data / -presentation", "18 use cases", "Passkey · OTP · drafts · session · recognized account")
    Container(discoverF, "discover-domain / -data / -presentation", "Fade Yield feed", "Hero + five sections, desktop & mobile views")
    Container(searchF, "search-domain / -data / -presentation", "The unified ledger", "One ranked list across five kinds.\nHas its own domain now — it was a bare\ntab surface at the last revision")
    Container(socialF, "social-domain / -data / -presentation", "Notes & community", "Tasting notes, the writing stream")
    Container(catalogF, "catalog · collections · contributions · lenses", "domain + data", "Wine · WineId · verdict & provenance vocabulary")
    Container(contentF, "editorial · events · provenance", "domain + data", "The pools Discover arranges")
    Container(cellarF, "cellar-presentation", "screens", "The one surface still without a domain")
  }

  Container_Boundary(sharedPkgs, "Shared Packages — 7 (workspace:*)") {
    Container(ds, "@shared/design-system", "Tokens · atoms · molecules", "Logical start/end spacing only,\nso RTL is a config flip")
    Container(pcore, "@shared/presentation-core", "View / ViewModel protocol · responsive")
    Container(loc, "@shared/localization", "i18next + FormatJS polyfill-force", "7 locales x 9 namespaces.\nThe only package other layers\nimport for i18n")
    Container(net, "@shared/networking", "Fetch · auth header · retry", "Sends Accept-Language, reads Content-Language")
    Container(persist, "@shared/persistence", "KeyValueStorage seam — local / in-memory")
    Container(flags, "@shared/feature-flags", "Capability gating")
    Container(obs, "@shared/observability", "Structured client logging")
  }

  Container(config, "Configuration", "TypeScript", "BuildFlavor x AppEnvironment · EnvironmentResolver")
  Container(doubles, "App Doubles", "non-release only", "Per-repository doubles: catalog · discover · member · places")
}

System_Ext(backend, "Kgwari API", "API Gateway + Lambda")
System_Ext(cognito, "Amazon Cognito", "Passkeys · OTP · JWT")
ContainerDb_Ext(s3, "S3 Bucket", "Static assets", "Expo web export")
System_Ext(cf, "CloudFront", "CDN distribution")
System_Ext(ghpkg, "GitHub Packages", "@edwardseshoka/*")

Rel(member, cf, "Loads", "HTTPS")
Rel(cf, s3, "Origin", "HTTPS")
Rel(shell, composition, "Resolves components from")
Rel(composition, features, "Wires domain to data")
Rel(shell, features, "Mounts feature screens")
Rel(features, sharedPkgs, "Composes UI · fetches · persists")
Rel(net, backend, "GET / POST / PATCH", "HTTPS / JSON")
Rel(features, cognito, "Passkey & OTP", "via member-data adapters")
Rel(config, composition, "Selects API base URL & doubles per env")
Rel(doubles, composition, "Substituted at the repository boundary", "non-release")
Rel_U(ghpkg, features, "Provides contracts · foundation · samples · capabilities", "build-time")

@enduml

1C — ComponentInside the member feature slice

Still the richest slice in the repo: eighteen use cases behind seven ports, with passkeys, an OTP fallback, third-party provider auth, unsent drafts and account recall each entering through a different adapter but leaving through the same domain.

fe-component diagram
PlantUML source
@startuml
!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml

LAYOUT_WITH_LEGEND()
HIDE_STEREOTYPE()
skinparam backgroundColor #FFFFFF
skinparam defaultFontName "Inter"
skinparam shadowing false

title Kgwari Client — Component view of the "member" feature slice (passkey-first auth)

Container_Boundary(memberP, "@features/member-presentation") {
  Component(coordinator, "Onboarding Coordinator", "View + ViewModel", "Person-first flow; drives the adaptive step")
  Component(screens, "Screens", "React Native", "Desktop / mobile views · skeleton · error")
  Component(vm, "ViewModels", "TS + protocol", "One ViewModelInterface per screen — the test seam")
}

Container_Boundary(memberD, "@features/member-domain") {
  Component(authUC, "Auth use cases", "TypeScript", "RegisterPasskey · AuthenticateWithPasskey\nAuthenticateWithProvider · RequestVerificationCode\nSignInWithVerificationCode · SignOut")
  Component(memberUC, "Member use cases", "TypeScript", "CompleteRegistration · SaveMemberDraft\nGet/PatchMemberProfile · ValidateAdaptiveStep")
  Component(sessionUC, "Session & recall use cases", "TypeScript", "Save/Get/ObserveSessionState\nRemember / Get / ForgetRecognizedAccount")
  Component(ports, "Ports", "Interfaces", "MemberRepository · SessionRepository\nVerificationRepository · PasskeyClient\nOtpAuthClient · MemberDraftPersistence")
  Component(validators, "Validators", "TS", "Contact · verification code — shared with foundation")
}

Container_Boundary(memberData, "@features/member-data") {
  Component(repos, "Repositories", "Adapters", "MemberRepository · SessionRepository · VerificationRepository")
  Component(services, "Services", "Data access", "MemberService (HTTP) · AuthOtpService (HTTP)")
  Component(cognitoAdapter, "CognitoAuthAdapter", "SDK adapter", "Passkey assertion · token exchange\nCognitoTokenStore")
  Component(persistence, "Persistence", "TS", "MemberDraftPersistence · RecognizedAccountPersistence\nover Default*Storage")
  Component(mappers, "DTO mappers", "@edwardseshoka/foundation", "DTO <-> Entity, one mapper per direction")
  Component(dtos, "Wire DTOs", "@edwardseshoka/contracts", "AuthOtp · Member · MemberProfilePatch\nSession · RecognizedAccount")
}

Container_Ext(net, "@shared/networking", "fetch wrapper + auth header")
Container_Ext(kv, "@shared/persistence", "KeyValueStorage seam")
Container_Ext(doubles, "App Doubles", "non-release", "MemberRepositoryAppDouble\nVerificationRepositoryAppDouble\nAuthClientAppDoubles")
Container_Ext(backend, "Kgwari API", "/auth/otp/* · /members/me")
Container_Ext(cognito, "Amazon Cognito", "Passkeys · JWT")

Rel(coordinator, screens, "Presents")
Rel(screens, vm, "Bind to")
Rel(vm, authUC, "Invokes")
Rel(vm, memberUC, "Invokes")
Rel(vm, sessionUC, "Observes session through")
Rel(authUC, ports, "Depends on")
Rel(memberUC, ports, "Depends on")
Rel(sessionUC, ports, "Depends on")
Rel(memberUC, validators, "Validates with")
Rel(repos, ports, "Implement")
Rel(doubles, ports, "Implement (non-release)", "scenario-switched")
Rel(repos, services, "Delegate to")
Rel(repos, mappers, "Map with")
Rel(services, dtos, "Typed by")
Rel(services, net, "HTTP")
Rel(net, backend, "REST / JSON", "HTTPS")
Rel(repos, cognitoAdapter, "Passkey & token work")
Rel(cognitoAdapter, cognito, "SRP / WebAuthn", "AWS SDK")
Rel(persistence, kv, "Reads / writes through")
Rel(repos, persistence, "Drafts & recognized account")

@enduml
Technical Summary

A universal Expo application shipped as a pnpm-workspaces monorepo: twenty-seven feature packages across twelve domains — catalog, cellar, collections, contributions, discover, editorial, events, lenses, member, provenance, search, social — over seven shared packages for the design system, presentation core, localization, networking, persistence, feature flags and observability. Presentation is strictly MVVM: every screen has a ViewModelInterface, which is the seam the tests bind to instead of the React tree.

Two decisions carry most of the weight. The first is the runtime split between BuildFlavor and AppEnvironment: non-release builds expose an in-app environment switcher and pluggable App Doubles that substitute at the repository boundary, so a single deployable bundle can produce deterministic offline states for design review. The second is that persistence goes through a KeyValueStorage seam rather than touching platform storage directly — the same reason the member slice can test draft-saving and account recall without a browser.

Two of the twelve domains are documented on their own pages, because neither is only a client concern. Search grew a real domain here since the last revision — it was a bare tab surface with no domain of its own — and it now carries five use cases, two of which exist only on this side of the wire: recent search terms are recorded and read locally, and never travel. Localization is the other, and @shared/localization is the single package every other layer imports for it, enforced by lint rather than convention. See 04 / Search and 05 / Localization.