Lifecycle flows
First, one overall story per app — patient, provider, admin.
Then the AWS sections show how those APIs are hosted.
New resources are named DBP- plus Title-Case words.
The API is Python 3.14 on Amazon Linux 2023.
India · IST product, offset on the wire
50,000 bookings / day
APIs only — not the apps
Phase 1 EC2 · Phase 2 EKS
Python 3.14 on Amazon Linux 2023
Names: DBP-Api, DBP-Holds
Money = integer paise
00 — Three apps, one product
Read this as a clinic day. Provider sets up care. Patient books a token (not a clock time).
Admin approves people and watches money. Your APIs sit in the middle.
flowchart LR
Provider[Provider_app]
Patient[Patient_app]
Admin[Admin_portal]
API[Your_APIs]
Provider -->|clinic_sets_doctors_and_queue| API
Patient -->|search_pay_watch_token| API
Admin -->|approve_and_monitor| API
API -->|push_and_refunds| Patient
API -->|alerts| Provider
Patient appBase /api/patient. Phone OTP. Live immediately. Books for a family member. Pays, gets a token number, watches the queue.
Provider appBase /api/provider. One app, three hats: clinic front desk, doctor, diagnostic centre. Same phone can hold more than one hat.
Admin portalBase /api/admin. Humans approve clinics/doctors/centres, set fees, retry refunds. Not used by patients.
01 — Patient app (overall flow)
What a patient does from first open to leaving the clinic — and later, paying for lab tests.
1. Sign inPhone + OTP. Add yourself, family, place, allergies.
2. SearchPick a speciality or name. See doctors at nearby clinics and their slots.
3. HoldChoose who is visiting. Seat is reserved 8 minutes. No token yet.
4. PayOn success they get a token number. Failed pay can retry in the same 8 minutes.
5. Clinic dayWhen the desk starts the doctor, watch “now serving”. Walk in on your number.
6. AfterCancel before 7:00 am that day (full refund), or approve lab tests the doctor ordered.
flowchart TD
OTP[Open_app_phone_OTP]
Profile[Add_family_and_area]
Search[Search_doctor_or_clinic]
Hold[Hold_seat_8_minutes]
Pay[Pay]
Token[Get_token_number]
Queue[Watch_now_serving]
Seen[See_the_doctor]
Cancel[Cancel_before_7am_refund]
Offer[Doctor_unavailable_accept_or_decline]
Labs[Pay_for_lab_tests]
OTP --> Profile --> Search --> Hold --> Pay --> Token
Token --> Queue --> Seen
Token --> Cancel
Token --> Offer
Seen --> Labs
Simple ruleNo payment = no token. The number is given only after money is captured.
If the doctor drops outClinic may offer another doctor. Patient has 4 hours. Decline or silence = full refund.
Missed turnIf they were called and not there, or the desk closed the queue, there is no refund.
02 — Provider app (overall flow)
One mobile app. After OTP, the person picks a role. Clinic, doctor and diagnostic centre
each wait for admin approval before they can work.
flowchart TD
OTP[Phone_OTP]
Role[Choose_role]
Apply[Fill_details_and_documents]
Wait[Wait_for_admin]
Clinic[Clinic_desk]
Doctor[Doctor]
Centre[Diagnostic_centre]
OTP --> Role --> Apply --> Wait
Wait -->|approved| Clinic
Wait -->|approved| Doctor
Wait -->|approved| Centre
Wait -->|rejected| Apply
Clinic desk
1. SetupPublish specialities. Add a doctor with their DOC code.
2. TimingsSet which days and hours each doctor sits, and how many tokens.
3. TodayCheck the doctor in. Queue goes live for patients.
4. ConsoleCall next, skip if absent, recall later, then close.
5. If doctor cancels6 hours to offer another doctor, or everyone is refunded (clinic pays).
6. Remove doctorTakes effect after 7 days. App shows the cost first.
flowchart LR
Spec[Add_specialities]
Attach[Attach_doctor]
Hours[Set_hours_and_token_limit]
Checkin[Check_in_today]
Call[Call_next]
Close[Close_queue]
Spec --> Attach --> Hours --> Checkin --> Call --> Close
Doctor
1. CodeAfter approval they get a DOC id to give clinics.
2. Join clinicsAccept invites. One doctor can sit at many clinics.
3. CalendarSee appointments per clinic and date.
4. Leave / awayBlock new bookings, or mark a session unavailable.
5. TestsPick tests first, then one or more centres that cover them.
flowchart LR
Code[Get_DOC_code]
Join[Join_clinics]
See[See_appointments]
Away[Leave_or_unavailable]
Tests[Order_lab_tests]
Code --> Join --> See
See --> Away
See --> Tests
Diagnostic centre
1. ServicesTurn on tests from the admin catalogue and set a price.
2. IncomingSee an order only after the patient has paid that centre.
3. WorkCollect sample → process → mark reported.
4. HolidaysBlock dates so new bookings cannot land.
flowchart LR
Offer[List_tests_and_prices]
Paid[Paid_order_appears]
Work[Collect_process_report]
Offer --> Paid --> Work
Same phone, two hatsA doctor who owns a clinic switches role in the app. The server issues a new login token for that hat.
Desk wifi dropsCall-next is saved on the phone and sent again. The API must accept the same action twice safely.
Centre privacyThey see name and tests. They must not see allergies or medical history.
03 — Admin portal (overall flow)
A browser app for the platform team. They do not treat patients. They unlock providers and keep money honest.
1. TodayHome shows what needs a human: new applications, stuck refunds, open reassignments.
2. VerifyOpen a clinic/doctor/centre file. Approve, or reject one document with a reason.
3. CatalogueSpecialities, fee bands, lab tests and price ceilings.
4. WatchSessions, bookings, lab orders — read-only monitors.
5. MoneyRetry a failed refund. Mark a clinic settlement paid with a UTR.
6. PolicyHold minutes, 6-hour / 4-hour windows, 7-day removal notice.
flowchart TD
Home[Open_admin_home]
Verify[Approve_or_reject_application]
Code[Provider_gets_CLN_or_DOC_or_DGC_code]
Watch[Watch_bookings_and_queues]
Money[Refunds_and_settlements]
Policy[Change_platform_numbers]
Audit[Everything_is_logged]
Home --> Verify --> Code
Home --> Watch
Home --> Money
Home --> Policy
Verify --> Audit
Money --> Audit
Policy --> Audit
ApproveIssues the unique code and freezes legal name and registration number.
RejectMust name the document and quote a note. Applicant can fix and re-apply (3 times, then a phone call).
Looking up a patientIs itself an audit event. Admin sees little health detail on purpose.
10 — What your APIs must host
Both phases
Flutter apps and the admin UI are other teams. One CloudFront:
doctors.rohimatech.com
(distribution d3h3h0bro93hk1).
/ is docs. /api is the backend, then
/patient, /provider, or /admin
— plus /internal and /webhooks.
The API process is EC2 in Phase 1 and an EKS pod in Phase 2. Prefixes do not change.
flowchart LR
subgraph apps [Not_AWS]
P[Patient_app]
V[Provider_app]
A[Admin_UI]
end
subgraph edge [Always_the_same]
CF[doctors_rohimatech_com]
APIGW[DBP-Api_Gateway]
Cog[DBP-Users]
end
subgraph api [API_process_EC2_or_EKS]
PatAPI["/api/patient/*"]
ProvAPI["/api/provider/*"]
AdminAPI["/api/admin/*"]
Jobs["/api/internal/*"]
Hooks["/api/webhooks/*"]
end
P --> CF
V --> CF
A --> CF
CF --> APIGW
APIGW --> Cog
APIGW --> PatAPI
APIGW --> ProvAPI
APIGW --> AdminAPI
APIGW --> Jobs
APIGW --> Hooks
Idempotency-KeyRequired on every POST that moves money or the queue. Stored in DynamoDB, not Redis.
ClocksEventBridge → SNS → /api/internal/jobs. Same URL in both phases. Gateway posts /api/webhooks/payments.
Phase 1 is not EKSPersonal-dev compute is one EC2. Production aim is EKS.
11 — Shared AWS (do not rebuild for Phase 2)
Both phases
These services are the product. Only the box that runs the container changes. If the API only talks to these names via env vars, the cutover is a Terraform module swap.
flowchart TB
CF[CloudFront]
APIGW[HTTP_API_Gateway]
Cog[Cognito]
Image[ECR_dbp_api_image]
RDS[RDS_Postgres]
DDB[DynamoDB]
S3[S3]
EB[EventBridge_Scheduler]
SNS[SNS]
P1[Phase1_EC2]
P2[Phase2_EKS]
CF --> APIGW
APIGW --> Cog
Image --> P1
Image --> P2
APIGW -->|phase1_HTTP_EIP| P1
APIGW -->|phase2_VPC_Link| P2
P1 --> RDS
P1 --> DDB
P1 --> S3
P2 --> RDS
P2 --> DDB
P2 --> S3
EB --> SNS --> APIGW
| AWS service | Phase 1 | Phase 2 |
| CloudFront | Created | Keep |
| HTTP API Gateway + Cognito | HTTP integration to EC2 EIP | Same API, integration → VPC Link |
ECR image dbp-api (lowercase required) | Python 3.14 on AL2023, process DBP-Api | Same image tag on EKS |
| RDS / DynamoDB / S3 | Created | Keep (maybe larger RDS) |
| EventBridge + SNS | Created | Keep |
| EC2 + Elastic IP | Created | Destroy after cutover |
| EKS + NLB | Not created | Created |
12 — Phase 1 developing: CloudFront → API Gateway → EC2
Phase 1
Personal-developer stack. No EKS control plane. API Gateway HTTP API proxies to one machine. This is a supported AWS path: HTTP integration, not VPC Link.
1. CloudFrontdoctors.rohimatech.com — / is docs, /api is the backend then /patient /provider /admin. TLS. Cache queue later.
2. API GatewayDBP-Api + Cognito DBP-Users. HTTP proxy to the Elastic IP.
3. One EC2DBP-Api on t4g.small. Docker image dbp-api, Python 3.14, port 8080.
4. DataRDS + DynamoDB + S3. Identical to Phase 2.
5. ClocksEventBridge → SNS → /api/internal/jobs.
6. Not hereNo EKS, NLB, NAT, WAF, KMS, Redis.
flowchart TB
Apps[Apps]
CF[CloudFront]
GW[HTTP_API_Gateway]
Cog[Cognito]
EIP[Elastic_IP_port_8080]
EC2[EC2_t4g_small_Docker]
RDS[RDS_micro]
DDB[DynamoDB]
S3[S3]
EB[EventBridge]
SNS[SNS]
Apps --> CF
CF -->|cache_hit| Apps
CF -->|miss_or_write| GW
GW --> Cog
GW -->|HTTP_proxy| EIP --> EC2
EC2 --> RDS
EC2 --> DDB
EC2 --> S3
EC2 --> EB
EB --> SNS
SNS -->|HTTPS_jobs| GW
sequenceDiagram
participant App
participant CloudFront
participant APIGateway
participant Cognito
participant EC2
participant RDS
participant DynamoDB
App->>CloudFront: HTTPS
CloudFront->>APIGateway: origin
APIGateway->>Cognito: verify_JWT
Cognito-->>APIGateway: ok
APIGateway->>EC2: HTTP_to_Elastic_IP_8080
EC2->>RDS: bookings
EC2->>DynamoDB: hold_or_queue
EC2-->>App: JSON
NamesCapitals allowed: DBP-Vpc, DBP-Api-Sg, DBP-Holds. Forbidden: dbp-postgres, dbp-api, dbp-kyc-….
Bill while codingAbout $25–35/month if EC2 + RDS stay up. No $73 Kubernetes tax.
Elastic IPRequired so Stop then Start does not change the API Gateway URL.
EC2 SGPort 8080 is reachable; Cognito and X-Jobs-Secret are the real locks in Phase 1.
13 — Phase 2 production: CloudFront → API Gateway → EKS
Phase 2
Same door, same API contract. Only the hop after API Gateway changes: VPC Link → NLB → pod. This is the earlier production proposal.
flowchart TB
Apps[Apps]
CF[CloudFront]
GW[HTTP_API_Gateway]
Cog[Cognito]
NLB[NLB]
EKSCP[EKS_control_plane]
Pod[API_pod_same_image]
RDS[RDS]
DDB[DynamoDB]
S3[S3]
EB[EventBridge]
SNS[SNS]
Apps --> CF
CF --> GW
GW --> Cog
GW -->|VPC_Link| NLB --> Pod
EKSCP -.-> Pod
Pod --> RDS
Pod --> DDB
Pod --> S3
EB --> SNS --> GW
sequenceDiagram
participant App
participant CloudFront
participant APIGateway
participant Cognito
participant NLB
participant Pod
participant RDS
participant DynamoDB
App->>CloudFront: HTTPS
CloudFront->>APIGateway: origin
APIGateway->>Cognito: verify_JWT
Cognito-->>APIGateway: ok
APIGateway->>NLB: VPC_Link
NLB->>Pod: HTTP
Pod->>RDS: bookings
Pod->>DynamoDB: hold_or_queue
Pod-->>App: JSON
EKS control planeAWS-managed Kubernetes brain. ~$73/month. Not on the request path. Worth it only when you want production scheduling and more than one node.
EC2 from Phase 1Turned off and destroyed after the flip. Do not run both origins for long.
14 — Smooth cutover (how the code stays aligned)
Both phases
Infra is different. The process is not. If you follow this, Phase 2 is a module apply + one API Gateway edit.
flowchart LR
Code[API_source]
Docker[Dockerfile]
ECR[ECR_dbp_api]
Env[Env_vars_only]
IAM[Same_IAM_policy]
P1[EC2_instance_role]
P2[EKS_IRSA]
Code --> Docker --> ECR
Code --> Env
IAM --> P1
IAM --> P2
ECR --> P1
ECR --> P2
| Rule | Why it makes Phase 2 easy |
One image, listen on PORT | docker run and a Kubernetes container are the same binary. |
| No Kubernetes API in code | Nothing to rip out later. |
DATABASE_URL, table names, bucket, Cognito IDs from env | Phase 2 keeps the same RDS / DDB / S3 / user pool. |
| AWS SDK default credential chain | EC2 instance profile now, IRSA later, same policy JSON. |
IaC: shared + compute-ec2 XOR compute-eks | Only the API Gateway integration URI is the switch. |
SNS always calls /api/internal/jobs | Clocks never point at an EC2 IP or a pod IP. |
Cutover dayApply compute-eks → deploy the same image tag → flip API Gateway to VPC Link → health check → stop EC2 → destroy compute-ec2.
Do notPut Postgres only on the EC2 disk. Use RDS from day one or you will migrate data by hand.
15 — Start / Stop / Destroy
Phase 1
Three operations on tagged resources Project=dbp. Use these instead of clicking around the console.
flowchart TD
Start[START]
Stop[STOP]
Destroy[DESTROY]
Start --> EC2up[Start_EC2]
Start --> RDSup[Start_RDS]
Start --> EBon[Enable_EventBridge_group]
Start --> Health[GET_internal_health]
Stop --> EBoff[Disable_EventBridge_group]
Stop --> EC2down[Stop_EC2]
Stop --> RDSdown[Stop_RDS]
Stop --> Keep[Keep_CF_APIGW_Cognito_DDB_S3]
Destroy --> Snap[Optional_RDS_snapshot]
Destroy --> TF[terraform_destroy]
Destroy --> Zero[Bill_goes_to_zero]
| Command | What happens | What you still pay |
make start | EC2 + RDS start. Elastic IP still attached. Clocks enabled. API Gateway URL unchanged. | Instance hours again. |
make stop | Clocks disabled first (so SNS does not retry a dead box). EC2 stopped. RDS stopped. | EIP (~$3.6) + RDS storage (~$2) + pennies. No EKS bill. |
make destroy | Empty S3 if needed, then destroy the stack. Cognito users and DDB items go away. | $0 |
RDS stop limitAWS auto-starts a stopped RDS instance after 7 days. Re-run stop, or destroy if you are away longer.
Phase 2 equivalentsScale the node to 0 (control plane still ~$73) or delete the cluster to truly stop paying for EKS.
16 — Login / OTP: Cognito consumption
Both phases Cognito holds the user. Tiny Lambdas send the OTP through MSG91 or WhatsApp. DynamoDB counts attempts. RDS gets a PatientProfile only after the first successful verify.
flowchart TD
App[App_sends_phone]
CF[CloudFront]
GW[API_Gateway]
Cog[Cognito_custom_auth]
L[CreateAuthChallenge_Lambda]
Vendor[MSG91_or_WhatsApp]
Lock[DynamoDB_OtpLock]
Verify[VerifyAuthChallenge_Lambda]
JWT[Cognito_issues_JWT]
Pod[API_GET_patient_me]
RDS[RDS_PatientProfile]
App --> CF --> GW --> Cog
Cog --> L --> Vendor
L --> Lock
App --> Verify
Verify --> Lock
Verify --> JWT
JWT --> Pod --> RDS
ConsumedCognito DBP-Users, 2–3 Lambdas, DynamoDB DBP-Otp-Lock, then DBP-Api (EC2 or EKS) + RDS for /api/patient/me or /api/provider/memberships.
Not consumedCognito SMS, SNS SMS, ElastiCache.
17 — Hold → pay → token: what each service stores
Both phases A seat lives in DynamoDB for 8 minutes. The exact expiry is an EventBridge one-time schedule, not DynamoDB TTL. The booking and token number live in RDS only after payment capture.
flowchart TD
Search[GET_patient_search]
Hold[POST_patient_holds]
Pay[Payment_app]
WH[POST_webhooks_payments]
Book[POST_patient_bookings]
Exp[EventBridge_8_min]
Search --> RDS
Hold --> DDBHold[DynamoDB_Holds]
Hold --> DDBIdem[DynamoDB_Idempotency]
Hold --> Exp
Exp -->|SNS_expire| Release[API_release_seat]
Pay --> Gateway[Razorpay_or_Cashfree]
Gateway --> CF2[CloudFront_webhook]
CF2 --> WH
WH --> Book
Book --> DDBHold
Book --> RDSBook[RDS_Booking_and_Token]
Failed paymentDoes not consume a release. Hold row stays. Patient retries inside the same 8 minutes.
CaptureRDS write is the only moment a token number exists.
DynamoDB TTLMay delete the hold row later as garbage. Must not be the product clock.
18 — Live queue: CloudFront cache vs DynamoDB
Both phases High-volume path. Patients poll every ~10s. Most polls should never reach EC2/EKS or RDS.
flowchart TD
Poll[GET_patient_sessions_queue]
CF[CloudFront]
Hit[Cache_HIT_2_to_3s]
Miss[Cache_MISS]
GW[API_Gateway]
Pod[API_process]
View[DynamoDB_QueueView]
Desk[Front_desk_call_next]
Poll --> CF
CF --> Hit
Hit --> Poll
CF --> Miss
Miss --> GW --> Pod --> View
View --> CF
Desk --> Pod
Pod -->|overwrite_snapshot| View
Pod -->|next_poll_sees_new_value| CF
Cache hitConsumes CloudFront only. This is how 50k bookings/day stay cheap.
Cache missConsumes API Gateway + EC2 or EKS + DynamoDB. Still does not hit RDS for now_serving.
Desk writecall-next / skip / check-in always hits the API process + DynamoDB + RDS token state. CloudFront cache key ignores Authorization.
19 — Background clocks: EventBridge + SNS
Both phases No extra worker. AWS wakes the same API via CloudFront. SNS retries if the process returns 5xx.
flowchart LR
subgraph create [API_creates_a_clock]
Hold[Hold_created]
Unavail[Doctor_unavailable]
Night[Nightly_rule]
end
subgraph awsClock [AWS_clock]
EB[EventBridge_Scheduler]
SNS[SNS_topic]
end
subgraph consume [Same_API_process]
Jobs["POST /api/internal/jobs"]
RDS2[RDS]
DDB2[DynamoDB]
Push[FCM_or_SMS_vendor]
end
Hold --> EB
Unavail --> EB
Night --> EB
EB --> SNS
SNS --> Jobs
Jobs --> RDS2
Jobs --> DDB2
Jobs --> Push
| Clock | When EventBridge fires | SNS topic | What the API consumes next |
| Hold expiry | Exactly at expires_at | DBP-Holds | DynamoDB DBP-Holds delete; RDS unchanged if unpaid |
| Clinic 6h | T+6h if no proposal | DBP-Reassign | RDS refunds; notify topic |
| Patient 4h | T+4h per offer | DBP-Reassign | RDS refund or leave moved booking |
| Materialise | Nightly | DBP-Ops | RDS insert sessions |
| Notify | Immediate publish | DBP-Notify | Outbound FCM / WhatsApp from the public EC2 or EKS node |
20 — Files and payment webhooks
Both phases KYC bytes go straight to S3. The API only mints a signed URL. Payments never trust the app — the gateway calls CloudFront.
flowchart TD
subgraph kyc [Verification_docs]
App1[Provider_app]
Pod1[API_presign]
S3[S3_dbp_dev_docs]
App1 -->|GET_signed_PUT| Pod1
Pod1 --> S3
App1 -->|PUT_file_direct| S3
Admin[Admin_reviews] -->|GET_signed| S3
end
subgraph pay [Payment]
App2[Patient_checkout]
Razor[Payment_gateway]
CF3[CloudFront_no_cache]
GW3[API_Gateway_no_Cognito]
Pod2[API_capture]
RDS3[RDS_token]
App2 --> Razor
Razor --> CF3 --> GW3 --> Pod2 --> RDS3
end
S3SSE-S3 only. Objects stay in Standard. No Glacier transition. No KMS key.
WebhookPOST /api/webhooks/payments. CloudFront cache off. Signature verified in the API. Then the same hold → booking path.
Why compute is public in Phase 1No NAT. EC2 (later the EKS node) must reach the payment gateway, FCM, SMS vendor, and ECR by itself.
21 — Bill by phase
Mumbai, light personal use. Phase 1 is what you run now. Phase 2 is only when production needs Kubernetes.
| Resource | Phase 1 coding | Phase 1 stopped | Phase 1 destroyed | Phase 2 prod-small |
| EKS control plane | Not created | — | — | ~ $73 always on |
| NLB | Not created | — | — | ~ $16 |
| Compute | 1 × t4g.small ~ $12 | $0 (EC2 stopped) | $0 | Node(s) ~ $30 |
| Elastic IP | Free while EC2 is running | ~ $3.6 if kept | $0 | Not needed |
| RDS t4g.micro | ~ $12 | ~ $2 storage | $0 | ~ $12+ (Multi-AZ later) |
| CloudFront + APIGW + Cognito + DDB + S3 + clocks | Pennies | Pennies | $0 | Grows with traffic |
| NAT / WAF / Redis / KMS | Not created | — | — | Optional later |
| Total (order of magnitude) | ~$25–35 | ~$8–12 | $0 | ~$110–140 |
Personal developerStay on Phase 1. Use Stop after hours. Use Destroy if you will not touch it for weeks.
Why Phase 2 costs moreEKS control plane + NLB. That is the production tax, not a developing tax.
Regionap-south-1 only. Health and KYC stay in India.