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.
@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", "morara-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 / beta / production)") System_Ext(ghpkg, "GitHub Packages", "@edwardseshoka contracts · foundation · samples · places · phone-number-validation") 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") @enduml
Twenty-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.
@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 (24 workspace packages, one bundle)
Person(member, "Member", "Web / iOS / Android")
System_Boundary(fe, "morara-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 — 9 domains, 19 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(catalogF, "catalog-domain / -data", "Wine · WineId", "Verdict & provenance vocabulary")
Container(contentF, "editorial · events · provenance · social", "domain + data", "The pools Discover arranges")
Container(surfaceF, "cellar- / search- presentation", "screens", "Tab surfaces with no domain of their own yet")
}
Container_Boundary(sharedPkgs, "Shared Packages (workspace:*)") {
Container(ds, "@shared/design-system", "Tokens · atoms · molecules")
Container(pcore, "@shared/presentation-core", "View / ViewModel protocol · responsive")
Container(loc, "@shared/localization", "i18next + formatjs polyfills")
Container(net, "@shared/networking", "Fetch · auth header · retry")
Container(persist, "@shared/persistence", "KeyValueStorage seam — local / in-memory")
}
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")
@endumlmember feature sliceThe richest slice in the repo: eighteen use cases behind six ports, with passkeys, an OTP fallback, unsent drafts and account recall each entering through a different adapter but leaving through the same domain.
@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")
@endumlA universal Expo application shipped as a pnpm-workspaces monorepo: nineteen feature packages across nine domains — catalog, cellar, discover, editorial, events, member, provenance, search, social — over five shared packages for the design system, presentation core, localization, networking and persistence. 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.