~/maber.io · main open to work · remote · colorado springs, co
01.

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.

TYPE
Full-stack · Cloud
YEAR
2026
STACK
SvelteKit · TypeScript · Azure Functions · Cosmos DB · API Management · Terraform
STATUS
Live · Active development
ROLE
Solo · Architecture, IaC, frontend, ops
[ LIVE DEMO ] See it running pcpc.maber.io view source on github
/architecture

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 --> C

A 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

decisions worth reading

A:

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.

A:

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.

A:

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.

A:

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.

A:

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

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
    }
  };
}
view full file on github

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
}
view full file on github

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"
      ]
    }
  }
}
view full file on github