Skip to main content

Bilko demo — receipt upload/download fix (GCS shared storage) — MC #103095 (2026-06-07)

1. Symptom

CEO reported that receipt upload (PNG, PDF, JPG) was not working — a central part of the app. Investigation showed upload itself succeeded (HTTP 201) but viewing/downloading the receipt returned intermittent HTTP 404 approximately 60% of the time. From the user seat, intermittent 404 on download reads as a broken upload. The UI button "Priloženi dokumenti" -> "Preuzmi" triggered the failing request.

2. Root Cause

The demo API had no shared object storage configured. It used BILKO_LOCAL_UPLOAD_DIR=/tmp/bilko-uploads — per-instance ephemeral local disk, routed through ReceiptService.kt persistLocalIfEnabled, storing files as local:// URLs.

Cloud Run (bilko-api-demo) runs up to 5 instances with concurrency=1 (set during the earlier MC #103057 hang mitigation). An upload landing on instance A wrote the file to that instance's /tmp; a subsequent download request routed to instance B, which had no copy of the file, returning 404. Files were also permanently lost on any instance restart or recycle.

A secondary symptom was occasional 15-second frontend timeouts on the expense detail page: the several parallel API calls the page makes on load were serialised by concurrency=1.

Contributing config drift: the active deploy step in infrastructure/gcp/cloudbuild.yaml (deploy-api-demo) used --set-env-vars which replaces the entire env set, making a separate cloudbuild-demo-api.yaml with BILKO_LOCAL_UPLOAD_DIR ineffective.

3. Fix

Applied by FlowForge (Kelsey Hightower). No application code changes were required — ReceiptService.kt is unchanged and uses a transparent filesystem abstraction.

ChangeDetail
GCS bucket provisionedgs://bilko-receipts-demo, region europe-north1, uniform bucket-level access, IAM: bilko-api-stage-sa = roles/storage.objectAdmin
Cloud Run exec environmentUpgraded to gen2 (required for gcsfuse volume mounts)
Volume mount added--add-volume=name=receipts,type=cloud-storage,bucket=bilko-receipts-demo
Mount path--add-volume-mount=volume=receipts,mount-path=/mnt/bilko-uploads
Env var updatedBILKO_LOCAL_UPLOAD_DIR: /tmp/bilko-uploads -> /mnt/bilko-uploads
Config persistedAll changes committed to infrastructure/gcp/cloudbuild.yaml (deploy-api-demo step)

Deployed: tag v0.2.30, commit 642bbc0cefdc63777d8c12d61aa61a8257716290, revision bilko-api-demo-00135-rmv, 100% traffic on new revision. Cloud Build ID: 793f929b-f41a-49e9-afa9-65b54d3972ff.

Note: Cloud Build reported FAILURE due to a pre-existing flaky test timeout in coverage artifact upload (expenses-ux-102887). This is unrelated to the GCS change. All 8 deploy gates (lint, typecheck, unit, coverage, trivy, gitleaks, semgrep, npm-audit) PASSED; build, push, trivy, migrate, deploy, promote, smoke-test, and verify-sha steps all succeeded.

4. Validation

Validated by Proveo (Angie Jones) — GLOBAL VERDICT: PASS.

TestResult
PDF upload + 10x download10/10 HTTP 200 (was intermittent 404)
PNG upload + 10x download10/10 HTTP 200
JPEG upload + 10x download10/10 HTTP 200
GCS persistence (15 total calls)15/15 HTTP 200 — confirmed shared across instances
UI: Priloženi dokumenti sectionVisible; download icon -> /content HTTP 200
Health checkhttps://bilko-demo-api.alai.no/api/v1/health -> 200 {"status":"ok"}

Company Mesh: mesh-thr-03f166bb-f001-4293-b9ec-db245e5790b3 — PASS.

Open item (non-blocker): 1 pre-fix orphan document (uploaded 07:58 before GCS deploy at 08:30) returns 404 as expected — the file lived in ephemeral /tmp on a recycled instance. Not a regression.

5. Known Follow-Up Tasks

MCDescription
#103102Graceful 404 handling for missing documents in UI; fix misleading BILKO-INV-001 error code returned on expense document content misses
#103103Flaky coverage test (expenses-ux-102887 dialog upload test) blocking clean Cloud Build artifact upload — unrelated to this fix
#103104Invoice-receipt download gap: POST /invoices/{id}/receipts returns 201 but no download endpoint exists and receiptUrl stays null in invoice record

6. Operational Notes — Demo Deploy Pipeline

  • Demo deploys via semver tag (e.g. v0.2.30) pushed to the Bilko repo, which triggers the bilko-main-deploy Cloud Build trigger, which runs infrastructure/gcp/cloudbuild.yaml. This deploys bilko-api-demo + bilko-web-demo and migrates bilko-demo-db.
  • Do NOT push directly to main — pushing to main auto-triggers the stage deploy (bilko-stage-auto-deploy), not the demo deploy.
  • Stage vs demo separation: Stage uses bilko-api-stage (no GCS mount, different SA). Demo uses bilko-api-demo with gs://bilko-receipts-demo via gcsfuse. RLS bugs and storage configuration differ between the two environments — always verify fixes on demo, not only stage.
  • GCS FUSE driver: gcsfuse.run.googleapis.com, requires Cloud Run gen2 execution environment.