Pokémon Card Price Checker
A full-stack Pokémon card pricing app built on Azure with a layered cache spanning three storage tiers — IndexedDB on the client, Cosmos DB Serverless on the server, and Scrydex as the origin. Each tier has independent staleness detection and refresh logic. Infrastructure is managed as 7 Terraform modules so a typo in one concern can't blow up another.
architecture
flowchart LR
C["Browser<br/>IndexedDB cache"]
APIM["Azure API Management<br/>rate limit · edge cache"]
F["Azure Functions<br/>SvelteKit endpoints"]
CDB[("Cosmos DB Serverless<br/>warm tier")]
SDX["Scrydex API<br/>origin"]
C -->|hot| C
C -->|miss| APIM
APIM --> F
F -->|warm| CDB
F -->|cold| SDX
SDX --> F
F --> APIM
APIM --> CA request for a price first hits the browser's IndexedDB cache. On miss, it goes to API Management, which enforces per-consumer rate limits and serves an edge-cached response when available. Cache miss falls through to a SvelteKit-on-Azure-Functions handler that reads warm data from Cosmos DB Serverless. A cold miss reaches Scrydex; the response writes back through every tier on its way home. Each tier's TTL is independently tunable so the hot/warm boundary moves with traffic shape rather than being locked at deploy time.
decisions worth reading
Read traffic is bursty and unpredictable. Standard's RU/s provisioning was overpaying for idle. Serverless is pay-per-RU, scales to zero between traffic spikes, and the cold-start overhead is acceptable for a non-realtime app.
Three reasons — rate limiting per consumer, response caching at the edge, and a stable contract surface that decouples client versioning from backend. APIM also gives me a single point for adding auth later without touching every Function.
Blast radius. A typo in a monolithic config can wipe production. Modules let me scope `terraform plan` to a single concern (networking, data, compute) and review changes in isolation. They're also reusable across environments — the same module powers dev and prod with different variable inputs.
Onboarding. New machine setup went from "install Node, pnpm, Azure CLI, Terraform, Bicep, configure tokens" to "open in DevContainer." Fifteen minutes to sixty seconds. The image is versioned alongside the code so old branches always have the toolchain they were built against.
Runes are component-scoped reactive state that survives across renders without store-subscription boilerplate. For a small app with localized state, runes have lower ceremony than the store pattern. Stores stay appropriate for cross-component shared state — see `pricing.svelte.ts` for that case.
code highlights
Boundary normalization for the Scrydex API
snake_case → camelCase happens at exactly one place — every layer above this works in the app's native shape, eliminating an entire class of casing bugs.
export function mapPaginatedResponse<T, U>(
raw: ScrydexPaginated<T>,
mapItem: (item: T) => U
): Paginated<U> {
return {
items: raw.data.map(mapItem),
pageInfo: {
hasNextPage: raw.has_more,
nextCursor: raw.next_cursor ?? null,
totalCount: raw.total_count
}
};
}Modular Terraform composition
Seven modules wired with explicit `depends_on` so blast radius is scoped per concern. Network changes don't need to think about Cosmos DB; data changes don't need to think about APIM.
module "network" {
source = "./modules/network"
prefix = local.prefix
tags = local.tags
}
module "data" {
source = "./modules/data"
prefix = local.prefix
subnet_id = module.network.private_subnet_id
tags = local.tags
depends_on = [module.network]
}
module "compute" {
source = "./modules/compute"
prefix = local.prefix
cosmos_endpoint = module.data.cosmos_endpoint
subnet_id = module.network.compute_subnet_id
tags = local.tags
depends_on = [module.data]
}
module "apim" {
source = "./modules/apim"
prefix = local.prefix
function_url = module.compute.function_url
tags = local.tags
}DevContainer with pre-built ACR image
The toolchain is versioned alongside the code. Checking out a branch from six months ago still gets the exact Terraform/Bicep/CLI versions it was built against.
{
"name": "pcpc",
"image": "maberacr.azurecr.io/devcontainers/pcpc:1.4.0",
"features": {
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
},
"postCreateCommand": "pnpm install --frozen-lockfile && pnpm prepare",
"remoteEnv": {
"AZURE_CONFIG_DIR": "${containerWorkspaceFolder}/.azure"
},
"customizations": {
"vscode": {
"extensions": [
"ms-azuretools.vscode-azurefunctions",
"hashicorp.terraform",
"svelte.svelte-vscode"
]
}
}
}