Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - SiyaMathe/vaxtrace-cloud: Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed. · GitHub
Skip to content

Repository files navigation

VaxTrace Cloud

Cloud-Native Vaccination Status Platform — Runs 100% Locally (No Azure Credits Required)

Skills Demonstrated: Azure Functions (HTTP + Queue triggers) · Azure Storage Queues · Azure Blob Storage · Azure SQL Database · .NET 8 · C# · Docker · Azurite (local Azure emulator) · CI/CD · IaC (Bicep) · REST API design


🧭 What This Is

VaxTrace Cloud is a cloud-native vaccination record processing platform that ingests vaccination data from multiple providers with different message formats, routes records through an Azure Storage Queue for asynchronous processing, archives raw payloads to Blob Storage, and persists structured records to an Azure SQL database — all queryable in seconds via an HTTP endpoint.

The system is designed to run entirely locally using Docker and Azurite (Microsoft's official Azure Storage emulator), eliminating the need for cloud credits while producing architecture and code identical to a real Azure deployment. When credits become available, swapping connection strings and pushing to main triggers the full CI/CD pipeline.


🗄️ Database Design (ERD)

The system uses a fully normalised schema to manage vaccination records, audit logs, and provider data. Below is the Entity Relationship Diagram (ERD):

VaxTrace Cloud ERD


🏗️ Architecture

┌──────────────────────────────────────────────────────────────────┐
│ VAXTRACE CLOUD │
│ │
│ POST /api/vaccination (HTTP-trigger Function) │
│ ↓ │
│ Azure Storage Queue (local: Azurite port 10001) │
│ "vaccination-queue" │
│ ↓ (fires automatically on message arrival) │
│ Queue-trigger Function (QueueProcessorFunction.cs) │
│ ↓ ↓ │
│ ┌──────────────────┐ ┌─────────────────────────────────────┐ │
│ │ Azure SQL DB │ │ Blob Storage (raw JSON archive) │ │
│ │ VaccinationRecord │ vaccination-raw-archive/{date}/ │ │
│ │ QueueMessageLog │ │ format{A|B}/{id}_{timestamp}.json │ │
│ │ (local: Docker) │ │ (local: Azurite port 10000) │ │
│ └──────────────────┘ └─────────────────────────────────────┘ │
│ ↓ │
│ GET /api/vaccination/{id} (HTTP-trigger Function) │
│ → queries SQL, returns full dose history in < 1 second │
└──────────────────────────────────────────────────────────────────┘

Two provider message formats supported:

Format A: Id:VaccinationCenter:VaccinationDate:VaccineSerialNumber
8001015009087:Groote Schuur Hospital:2024-01-15:PFZ-2024-001-A
Format B: VaccineBarcode:VaccinationDate:VaccinationCenter:Id
BAR-00123:2024-01-15:Groote Schuur Hospital:8001015009087

📁 Project Structure

vaxtrace-cloud/
├── backend/
│ ├── functions/
│ │ ├── HttpIngestFunction.cs # POST /api/vaccination — validates & queues message
│ │ ├── QueueProcessorFunction.cs # Queue-trigger — archives to Blob, upserts to SQL
│ │ ├── HttpQueryFunction.cs # GET /api/vaccination/{id} — status lookup
│ │ ├── HealthStatsAndBulkFunctions.cs # Health, Stats, and Bulk ingest endpoints
│ │ ├── MessageParser.cs # Format A & B detection and parsing
│ │ ├── Program.cs # Functions host setup and DI
│ │ ├── host.json # Queue polling, retry, and encoding config
│ │ ├── VaxTrace.Functions.csproj
│ │ └── local.settings.json.example # Local dev config (Azurite connection strings)
│ └── sql/
│ ├── 01_schema.sql # Normalised schema: Person, VaccinationRecord, QueueMessageLog
│ ├── 02_stored_procedures.sql # Upsert, query, log, stats procedures
│ └── 03_seed_data.sql # Hard-coded test IDs and pre-seeded records
├── scripts/
│ ├── setup-local.sh # One-command prerequisite check + stack setup
│ └── test-endpoints.sh # curl-based endpoint test suite
├── tests/
│ ├── VaxTrace.Tests.csproj
│ └── MessageParserTests.cs # 17 unit tests: Format A/B, edge cases, round-trips
├── infrastructure/
│ └── main.bicep # Azure IaC — Function App, SQL, Storage, App Insights
├── .github/
│ └── workflows/
│ ├── ci.yml # Build, unit tests, integration tests (Azurite + SQL Server)
│ └── cd.yml # Bicep deploy → schema migration → Function deploy → smoke test
├── .vscode/
│ ├── extensions.json # Recommended extensions
│ ├── launch.json # Debug configs for Functions and tests
│ ├── tasks.json # Build, docker-up, run-tests tasks
│ └── settings.json # mssql connection to local Docker SQL Server
├── requests.http # VS Code REST Client — all endpoints ready to fire
├── docker-compose.yml # SQL Server 2022 + Azurite + db-init + queue-init
├── .env.example
├── .gitignore
└── README.md

🚀 Local Setup (No Azure Credits Needed)

Prerequisites — install these once

ToolInstall
Docker Desktophttps://www.docker.com/products/docker-desktop
.NET 8 SDKhttps://dotnet.microsoft.com/download/dotnet/8.0
Azure Functions Core Tools v4npm install -g azure-functions-core-tools@4
VS Codehttps://code.visualstudio.com
VS Code ExtensionsOpen project → Ctrl+Shift+P → "Extensions: Show Recommended Extensions" → Install All

Step 1 — Clone & configure

git clone https://github.com/SiyaMathe/vaxtrace-cloud.git
cd vaxtrace-cloud
# Copy local settings (uses Azurite + Docker SQL by default)
cp backend/functions/local.settings.json.example backend/functions/local.settings.json
cp .env.example .env

Step 2 — Start the full local stack

docker-compose up -d

This single command starts four containers:

ContainerPurposePort
vaxtrace-azuriteAzure Storage emulator (Blob + Queue + Table)10000, 10001, 10002
vaxtrace-sqlSQL Server 2022 Developer Edition (free)1433
vaxtrace-db-initApplies all three SQL files automatically, then exits
vaxtrace-queue-initPre-creates queues and blob containers in Azurite, then exits

Wait ~30 seconds for SQL Server to be ready, then verify:

docker ps # vaxtrace-azurite and vaxtrace-sql should show "Up"

Step 3 — Restore packages and run tests

dotnet restore backend/functions/VaxTrace.Functions.csproj
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal

All 17 unit tests should pass without any services running (they test the parser only).

Step 4 — Start the Azure Functions

cd backend/functions
func start

You will see five functions register in the console:

Functions:
Health: [GET] http://localhost:7071/api/health
HttpIngestVaccination: [POST] http://localhost:7071/api/vaccination
HttpBulkIngestVaccination: [POST] http://localhost:7071/api/vaccination/bulk
HttpQueryVaccination: [GET] http://localhost:7071/api/vaccination/{id}
VaccinationStats: [GET] http://localhost:7071/api/vaccination/stats
QueueProcessorFunction: queueTrigger

Step 5 — Test the endpoints

Option A — VS Code REST Client (recommended)

Open requests.http in VS Code, install the humao.rest-client extension, then click Send Request above any block.

Option B — Shell script

# From a new terminal (keep func start running)
chmod +x scripts/test-endpoints.sh
./scripts/test-endpoints.sh

Option C — Quick curl test

# Health check
curl http://localhost:7071/api/health
# Query a pre-seeded fully vaccinated ID
curl http://localhost:7071/api/vaccination/0105215258021

📬 API Reference

MethodEndpointAuthDescription
GET/api/healthAnonymousService health — checks SQL and queue connectivity
POST/api/vaccinationAnonymousSubmit one vaccination record (Format A or B)
POST/api/vaccination/bulkAnonymousSubmit an array of records
GET/api/vaccination/{id}AnonymousQuery full vaccination status by SA ID or passport
GET/api/vaccination/statsAnonymousQueue throughput and vaccination coverage stats

POST /api/vaccination — JSON body (Format A)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: application/json" \
-d '{ "format": "A", "id": "0105215258021", "vaccinationCenter": "Groote Schuur Hospital", "vaccinationDate": "2024-01-15", "vaccineSerialNumber": "PFZ-2024-001-A" }'

Response (202 Accepted):

{
"status": "queued",
"messageId": "...",
"detectedFormat": "A",
"idNumber": "0105215258021",
"center": "Groote Schuur Hospital",
"date": "2024-01-15",
"message": "Record queued for processing. Query status at GET /api/vaccination/{id}"
}

POST /api/vaccination — raw message string (Format B)

curl -X POST http://localhost:7071/api/vaccination \
-H "Content-Type: text/plain" \
-d "BAR-00001:2024-02-12:Groote Schuur Hospital:0105215258021"

GET /api/vaccination/{id}

curl http://localhost:7071/api/vaccination/0105215258021

Response (200 OK):

{
"status": "FULLY_VACCINATED",
"idNumber": "0105215258021",
"idType": "SA_ID",
"name": "Siyabulela Mathe",
"vaccination": {
"totalDoses": 2,
"isFullyVaccinated": true,
"firstDoseDate": "2024-01-15",
"latestDoseDate": "2024-02-12",
"daysSinceLastDose": 108
},
"doses": [
{
"doseNumber": 1,
"vaccinationDate": "2024-01-15",
"vaccinationCenter": "Groote Schuur Hospital",
"serialNumber": "PFZ-2024-001-A",
"providerFormat": "A",
"isVerified": true
},
{
"doseNumber": 2,
"vaccinationDate": "2024-02-12",
"vaccinationCenter": "Groote Schuur Hospital",
"barcode": "BAR-00001",
"providerFormat": "B",
"isVerified": true
}
]
}

POST /api/vaccination/bulk

curl -X POST http://localhost:7071/api/vaccination/bulk \
-H "Content-Type: application/json" \
-d '[ "8001015009087:Charlotte Maxeke Hospital:2024-01-20:JNJ-2024-002-A", "BAR-00999:2024-03-05:Steve Biko Academic Hospital:0407145189089", "P12345678:Groote Schuur Hospital:2024-04-10:AZ-2024-200" ]'

🗄️ Database Schema

The SQL schema is fully normalised (3NF) with three core tables:

Person (1) ────────────< VaccinationRecord (*)
↓
QueueMessageLog (audit trail)
TablePurpose
PersonIdentity anchor — one row per SA ID or passport number
VaccinationRecordOne row per dose event with historical provider data snapshot
VaccinationCenterLookup — normalised center names seeded from incoming messages
VaccineLookup — vaccine product catalogue
QueueMessageLogComplete audit trail of every queue message received and its outcome

Key stored procedures:

ProcedureWhat it does
usp_UpsertVaccinationRecordIdempotent MERGE-based insert — replaying a duplicate message is a no-op
usp_GetVaccinationStatusReturns two result sets: person summary + all dose records
usp_LogQueueMessageAudit log insert at message receipt
usp_UpdateQueueMessageLogUpdates log with SUCCESS, DUPLICATE, or FAILED outcome
usp_GetProcessingStatsReturns three result sets: queue stats, recent failures, coverage counts

🔄 Message Processing Pipeline

When a message hits the queue, QueueProcessorFunction runs this pipeline:

1. Receive message from "vaccination-queue"
↓
2. Parse: detect Format A or B via MessageParser.cs
↓
3. Log to QueueMessageLog (SQL) — status: RECEIVED
↓
4. Archive raw JSON to Blob Storage
path: {year}/{month}/{day}/format{A|B}/{id}_{HHmmss}.json
↓
5. Call usp_UpsertVaccinationRecord (SQL stored procedure)
— idempotent: duplicate = no new row, returns existing RecordID
↓
6. Update QueueMessageLog — status: SUCCESS or DUPLICATE
↓
On any failure → status: FAILED → Azure retries up to 5 times
→ after 5 retries → dead-letter queue

🧪 Running Tests

# Unit tests only (no services needed)
dotnet test tests/VaxTrace.Tests.csproj --verbosity normal
# Integration tests (requires docker-compose up -d first)
dotnet test tests/VaxTrace.Tests.csproj --filter Category=Integration

Test coverage:

Test classCountWhat's tested
MessageParserTests17Format A parsing, Format B parsing, passport numbers, center names with colons, invalid dates, null/empty input, round-trip build→parse, all 5 seed messages as a Theory

☁️ Deploy to Azure (When You Get Credits)

Step 1 — Provision infrastructure

az login
az group create \
--name vaxtrace-rg \
--location southafricanorth
az deployment group create \
--resource-group vaxtrace-rg \
--template-file infrastructure/main.bicep \
--parameters sqlAdminPassword=YourSecurePassword123!

The Bicep template provisions on the Consumption plan (pay-per-execution — effectively free at low volume):

  • Azure Function App
  • Azure SQL Database (S0 tier)
  • Azure Storage Account (queues + blob containers)
  • Application Insights

Step 2 — Add GitHub Secrets

Go to your repo → Settings → Secrets and variables → Actions → New repository secret:

SecretHow to get it
AZURE_CREDENTIALSaz ad sp create-for-rbac --sdk-auth --role contributor --scopes /subscriptions/<id>
AZURE_RESOURCE_GROUPvaxtrace-rg
AZURE_FUNCTIONAPP_NAMEFrom Bicep output: functionAppName
SQL_SERVER_NAMEFrom Bicep output: sqlServerFqdn (without .database.windows.net)
SQL_ADMIN_USERsqladmin
SQL_ADMIN_PASSWORDThe password you chose above

Step 3 — Push to main

git push origin main

The CD pipeline runs automatically:

  1. Deploys Bicep infrastructure
  2. Applies SQL schema to Azure SQL
  3. Publishes and deploys the Function App
  4. Runs a smoke test against the live /api/health endpoint

🔍 Viewing Data Locally

Azure Storage Explorer (free desktop app)

  1. Download from https://azure.microsoft.com/features/storage-explorer
  2. Connect → Local emulator → Use development storage
  3. Browse queues: vaccination-queue, vaccination-deadletter
  4. Browse blob containers: vaccination-raw-archive, vaccination-processed

SQL Server in VS Code

  1. Install the mssql extension (in .vscode/extensions.json)
  2. Press Ctrl+Shift+PMS SQL: Connect
  3. Use the pre-configured connection: VaxTrace Local (Docker)
  4. Password: VaxTrace_Dev123!

📋 Pre-seeded Test IDs

The following SA ID numbers are seeded in 03_seed_data.sql and ready to query immediately:

SA IDNameStatus
0105215258021Siyabulela MatheFully vaccinated (2 doses)
8001015009087Thabo NkosiPartially vaccinated (1 dose — J&J)
9203224800088Ayanda DubeFully vaccinated (2 doses — AstraZeneca)
7512086150082Lerato MolefeNot vaccinated (person record only)
0407145189089Amahle ZuluNot vaccinated (person record only)
P12345678James SmithNot vaccinated (passport — foreign national)

🛠️ Troubleshooting

func start fails with "No job functions found"

dotnet build backend/functions/VaxTrace.Functions.csproj
# Then retry: func start

SQL connection refused

docker ps # check vaxtrace-sql is running
docker logs vaxtrace-sql # check for startup errors# SQL Server takes ~30s to be ready after first start

Azurite queue not found

docker logs vaxtrace-queue-init # check if queue creation ran# If it failed, re-run:
docker-compose up queue-init

Port 1433 already in use

# Stop any local SQL Server instance, or change the port in docker-compose.yml# Change: "1433:1433" to "1434:1433"# And update local.settings.json: Server=localhost,1434;...

About

Cloud-native vaccination record platform — Azure Functions, Queue triggers, Blob Storage & SQL. Runs 100% locally via Docker + Azurite. No cloud credits needed.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages