An HTTP/JSON API consumed by the Kgwari client. Identity is delegated to Cognito in both directions — the API verifies its JWTs, and Cognito calls back into a post-confirmation Lambda to create the member record.
@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 API — System Context Person(member, "Member", "Authenticated end user") Person_Ext(dev, "Developer / CI", "Deploys per-env CDK stacks; seeds dev data") System(backend, "Kgwari API", "kgwari-backend-app\nServerless TypeScript (dev · uat · beta · production)\n9 feature domains · 15 routes + a stream projector · 8 tables") System_Ext(frontend, "Kgwari Client", "Expo web / iOS / Android") System_Ext(cognito, "Amazon Cognito", "User pool · passkeys (verification REQUIRED)\nOTP broker · JWT issuer") System_Ext(opensearch, "Amazon OpenSearch", "The search ledger — managed or serverless\nper environment, behind one adapter seam") System_Ext(translate, "Amazon Translate", "On-demand translation of members' own words") System_Ext(bedrock, "Amazon Bedrock", "Wine-label parsing. NOT localization —\nthat is the frontend's build-time job") System_Ext(gha, "GitHub Actions", "OIDC-assumed deploy role · matrix per environment") System_Ext(ghpkg, "GitHub Packages", "@edwardseshoka contracts · foundation · samples") Rel(member, frontend, "Uses") Rel(frontend, backend, "Calls", "HTTPS / JSON · Bearer JWT") Rel(backend, cognito, "Validates JWTs · brokers OTP sign-in", "JWKS · cognito-idp admin") Rel(cognito, backend, "Post-confirmation trigger writes the member record", "Lambda") Rel(backend, opensearch, "Queries · indexes the projection", "SigV4 over REST") Rel(backend, translate, "TranslateText", "cached per content hash") Rel(backend, bedrock, "InvokeModel", "label parsing") Rel(dev, gha, "Pushes to main / dispatches deploy") Rel(gha, backend, "Synthesises & deploys CDK app", "AWS API") Rel(dev, backend, "Seeds dev tables", "npm run seed:dev") Rel_Back(backend, ghpkg, "Resolves shared packages at build", "npm install") @enduml
Every route Lambda's IAM grants and HTTP path are derived from the same row of a declarative table, so a route cannot be wired without the table access it needs. Two Lambdas are not routes at all: the Cognito post-confirmation trigger, and the projector that turns four table streams into search rows — see 04 / Search.
@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 API — Containers (per environment: kgwari-<env>, eu-west-1)
Person(member, "Authenticated member")
System_Ext(frontend, "Kgwari Client", "Expo client")
System_Boundary(be, "kgwari-backend-app — kgwari-<env>") {
Container_Boundary(authstack, "auth-stack") {
Container(userpool, "Cognito User Pool", "Amazon Cognito ESSENTIALS", "Passkeys — user verification REQUIRED\nJWT issuer · JWKS")
Container(postconfirm, "PostConfirmation trigger", "Lambda", "Writes the member record on sign-up")
}
Container_Boundary(apistack, "api-stack") {
Container(apigw, "HTTP API Gateway", "API Gateway v2", "JWT authorizer -> Cognito · custom domain")
Container_Boundary(lambdas, "15 route Lambdas — one bundle, one declarative route table") {
Container(catalogL, "Catalog", "4 lambda", "GET /public/wines · /public/featuredwine\n/public/collections · GET /wines/{id}")
Container(cellarL, "Cellar", "3 lambda · auth", "GET /cellar · POST /cellar · GET /cellar/{id}")
Container(discoverL, "getDiscover", "1 lambda", "GET /discover — reads 6 tables")
Container(searchL, "Search", "3 lambda · public", "GET /search · /search/suggest · /search/browse\n29s timeout on serverless only, to absorb\nthe collection's cold start")
Container(memberL, "Member", "2 lambda · auth", "GET /members/me (alias /user/profile)\nPATCH /members/me — partial by design")
Container(otpL, "OTP broker", "2 lambda · public", "POST /auth/otp/start · /auth/otp/verify\notpSession handle is the binding")
}
}
Container_Boundary(datastack, "data-stack") {
ContainerDb(ddb, "8 DynamoDB tables", "Amazon DynamoDB", "wines · members · provenance · editorial\nevents · social · discover-plan · search\nNames derived from one suffix map — no Fn::ImportValue")
Container(projector, "projectSearchRows", "Lambda · ONE function, FOUR streams", "wines · provenance · events · members\nTRIM_HORIZON · batchSize 10\nreportBatchItemFailures + bisect on error")
}
Container_Boundary(searchstack, "search-stack — its own stack, because a domain takes 15-30 min to change") {
Container(engine, "OpenSearch", "managed | serverless | none", "beta & production: managed domain\ndev & uat: NextGen collection, scales to zero\nSelected by EnvironmentConfiguration.search.flavour")
}
Container_Boundary(webstack, "web-stack") {
ContainerDb(s3, "Web Bucket", "S3", "Hosts the Expo web export")
Container(cf, "CloudFront Distribution", "CDN", "Exports BucketName & DistributionId")
}
Container(seed, "seedSampleData", "Node script", "npm run seed:dev — writes @edwardseshoka/samples\ninto the dev tables")
}
System_Ext(ghpkg, "GitHub Packages")
Rel(member, frontend, "Uses")
Rel(frontend, apigw, "REST", "HTTPS · Bearer JWT")
Rel(apigw, userpool, "Verifies JWT", "JWKS")
Rel(apigw, catalogL, "routes")
Rel(apigw, cellarL, "routes", "authorized")
Rel(apigw, discoverL, "routes")
Rel(apigw, searchL, "routes", "public")
Rel(apigw, memberL, "routes", "authorized")
Rel(apigw, otpL, "routes", "public")
Rel(catalogL, ddb, "Query / Get / Put", "AWS SDK v3")
Rel(cellarL, ddb, "Query / Get / Put")
Rel(discoverL, ddb, "Query x6 pools")
Rel(searchL, engine, "Search", "SigV4 · `es` managed, `aoss` serverless")
Rel(ddb, projector, "Streams changes to", "NEW_IMAGE")
Rel(projector, ddb, "Writes search rows", "write grant only")
Rel(memberL, ddb, "Get / Update members")
Rel(otpL, userpool, "AdminGetUser · AdminCreateUser", "cognito-idp")
Rel(otpL, ddb, "Reads & writes members")
Rel(userpool, postconfirm, "Invokes")
Rel(postconfirm, ddb, "Writes member")
Rel(seed, ddb, "BatchWrite", "dev only")
Rel_U(ghpkg, seed, "Provides sample content", "build-time")
Rel(cf, s3, "Origin")
@endumlGET /discoverThe one route that reads across six domains. The use case fans out six repository reads concurrently; a pure function then arranges the results into the feed. Nothing in the composer fetches, so the whole arrangement is testable without AWS.
@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 API — Component view of GET /discover (the cross-domain read)
Container_Boundary(presentation, "discover/presentation") {
Component(handler, "makeGetDiscoverHandler(components)", "Lambda handler", "Thin adapter — no branching beyond ok() / mapError()")
Component(respMapper, "toDiscoverContract", "Mappers", "Entity -> wire; hero · doorway · response mappers")
Component(routes, "discover.routes.ts", "Route constant", "The one place the path GET /discover is written")
}
Container_Boundary(composition, "Composition root — main/app/composition") {
Component(appComp, "AppComposition", "Factory", "Frozen DI graph built once per cold start")
Component(featureComp, "Feature compositions", "TS", "catalog · discover · member — each wires its own repositories")
Component(envCfg, "loadRuntimeEnvironmentConfiguration", "core/config", "Table names, region, user pool — read once")
}
Container_Boundary(domain, "discover/domain (+ five sibling domains)") {
Component(usecase, "GetDiscoverUseCase", "NoInputUseCase<Discover>", "Fans out six repository reads with Promise.all")
Component(composer, "composeDiscover", "Pure function", "Resolves the plan against the pools and orders the\nFade Yield funnel: editorial · wines · doorways · events · room")
Component(entities, "Entities", "TS", "Discover · DiscoverPlan · DiscoverGroup\nDiscoverHighlight · Doorway")
Component(ports, "Repository ports", "Interfaces", "plan · wines · regions · editorial · events · activities")
}
Container_Boundary(dataLayer, "*/data — one adapter pair per domain") {
Component(repos, "Dynamo*Repository", "Adapters", "Implement the six ports")
Component(services, "Dynamo*Service", "Table access", "Query / Get / Put against a single table each")
Component(recMappers, "Record mappers", "TS", "DynamoDB item <-> Entity")
}
Container_Boundary(core, "packages/core — eleven shared packages") {
Component(lambdaHttp, "lambda-http", "TS", "response · error.middleware · validate.middleware\napiGatewayEvent · JwtClaims")
Component(persistCore, "persistence", "TS", "partitionTable · fieldReaders")
Component(dynClient, "dynamodb", "@aws-sdk/lib-dynamodb", "Memoised document client")
Component(obs, "observability", "Powertools", "Structured JSON logger")
Component(domainCore, "domain", "TS", "Verdict · ProvenanceState · trust vocabulary")
}
ContainerDb_Ext(ddb, "6 DynamoDB tables", "wines · provenance · editorial\nevents · social · discover-plan")
System_Ext(contracts, "@edwardseshoka/contracts", "Wire DTOs + trust vocabulary")
Rel(routes, handler, "Names the path for")
Rel(handler, appComp, "Receives DI graph from")
Rel(appComp, featureComp, "Delegates wiring to")
Rel(appComp, envCfg, "Reads runtime config")
Rel(featureComp, repos, "Wires concrete adapters")
Rel(handler, usecase, "Invokes")
Rel(usecase, ports, "Depends on")
Rel(usecase, composer, "Arranges with")
Rel(composer, entities, "Produces")
Rel(composer, domainCore, "Ranks by Verdict from")
Rel(repos, ports, "Implement")
Rel(repos, services, "Delegate to")
Rel(repos, recMappers, "Map with")
Rel(services, persistCore, "Partition keys & field readers")
Rel(services, dynClient, "Uses")
Rel(dynClient, ddb, "Query", "HTTPS")
Rel(handler, respMapper, "Serialises through")
Rel(respMapper, contracts, "Shapes to DTOs in")
Rel(handler, lambdaHttp, "ok() / mapError()")
Rel(handler, obs, "Logs through")
@endumlA serverless TypeScript backend of nine feature domains — catalog, collections, discover, editorial, events, member, provenance, search, social — each split into data, domain and presentation, sitting on eleven packages/core libraries for HTTP adaptation, persistence, configuration, observability, the OpenSearch and Bedrock and Translate clients, and the shared trust vocabulary. Seven CDK stacks (auth, data, api, search, dns, web-certificate, web) provision it into four account-isolated environments in eu-west-1.
The infrastructure is written to remove whole classes of drift rather than to be clever. Physical table names come from one suffix map that both the creating and the consuming stack derive from, and consumers reference tables by derived name instead of Fn::ImportValue — which is what lets a data-stack change proceed without deadlocking its dependants. Routes, Lambdas and grants come from a single declarative table. A moved resource keeps its old address through an explicit alias path rather than a duplicated function, so already-shipped clients keep working while the new one ships.