Data & refresh

Deloc is built for static apps, but most dashboards need live numbers. Upload data files directly, or schedule a pipeline that pulls from BigQuery, Snowflake, Postgres, or any HTTP source into your app on a cron.

Data files

Any deployed app can store static data files alongside its HTML and JS. Your app fetches them with regular fetch — same origin, no CORS.

  • Supported types: .csv, .json, .tsv, .xml, .txt
  • Max size per file: 10MB
  • Filenames: alphanumeric plus . _ -; no dotfiles, no reserved names (index.html, manifest.json, etc.)
  • Storage counts toward your tier's total; bandwidth counts toward per-app limits

Uploading and refreshing

shell
# Single file
deloc upload-data sales-dash ./data.json

# Multiple files in one call
deloc upload-data sales-dash ./sales.csv ./forecast.csv ./config.json

# Rename on upload (single file only)
deloc upload-data sales-dash ./latest-run-8473.json --filename data.json

Re-uploading a file with the same name replaces it — no versioning. Your app picks up the new data on the next fetch. Cache-bust with a query string if you need immediacy:

ts
fetch("./data.json?t=" + Date.now())

Fetching from your app

ts
// In any page on the app subdomain
const resp = await fetch("./data.json");
const rows = await resp.json();

Same-origin requests, so no CORS headaches, no preflight. Reference from HTML the same way — <img src="./chart.png"> or <script src="./config.js"> all work.


Scheduled refresh — the big picture

For dashboards that need fresh numbers, pair your static app with a scheduled backend that queries your data source and uploads the result. Deloc's MCP server scaffolds the entire backend for you:

shell
# From Claude Code / Cursor / any MCP-enabled editor:
"Set up a BigQuery refresh for sales-dash that runs SELECT * FROM sales.daily"

# Or equivalently via the MCP tool:
# setup_data_refresh({ slug: "sales-dash", source: "bigquery", query: "SELECT ..." })

The tool writes four files into ./data-refresh/:

  • main.py — queries your source, POSTs JSON to your app
  • requirements.txt — Python deps
  • Dockerfile — Alpine Python 3.12, Cloud Run ready
  • README.md — scheduling instructions
What gets written to your app
The generated script POSTs the query result as JSON to /api/apps/{slug}/data, which stores it as data.json in your app's R2 bucket. Your app fetches it via fetch('./data.json') like any other static file.

Data sources

Pick the tab that matches your source:

main.py (run_query function)
import google.auth
from google.cloud import bigquery

GCP_PROJECT = os.environ.get("GCP_PROJECT", "my-project")

QUERY = """
SELECT region, SUM(revenue) AS total
FROM sales.orders
WHERE order_date >= CURRENT_DATE() - 30
GROUP BY region
"""

def run_query() -> list[dict]:
    credentials, _ = google.auth.default(
        scopes=["https://www.googleapis.com/auth/bigquery"],
    )
    client = bigquery.Client(project=GCP_PROJECT, credentials=credentials)
    job = client.query(QUERY)
    return [dict(row) for row in job.result()]

Env vars required:

  • GOOGLE_APPLICATION_CREDENTIALS — path to a service account JSON key
  • GCP_PROJECT — your GCP project ID

In Cloud Run, skip the key file and attach a service account with BigQuery Data Viewer + Job User roles instead.


Scheduling the refresh

Once you have the pipeline, you need something to run it on a schedule. The generated README.md covers three options:

The recommended production setup. Fully managed, logs in Cloud Logging, scales to zero.

shell
# 1. Build and push the image
docker build -t gcr.io/YOUR_PROJECT/deloc-refresh-sales .
docker push gcr.io/YOUR_PROJECT/deloc-refresh-sales

# 2. Create the Cloud Run Job
gcloud run jobs create deloc-refresh-sales \
  --image gcr.io/YOUR_PROJECT/deloc-refresh-sales \
  --set-env-vars DELOC_TOKEN=dl_xxxxx,DELOC_APP_SLUG=sales-dash,GCP_PROJECT=YOUR_PROJECT

# 3. Schedule it hourly
gcloud scheduler jobs create http deloc-refresh-sales-trigger \
  --schedule "0 * * * *" \
  --uri "https://YOUR_REGION-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/YOUR_PROJECT/jobs/deloc-refresh-sales:run" \
  --http-method POST \
  --oauth-service-account-email YOUR_SA@YOUR_PROJECT.iam.gserviceaccount.com

For BigQuery specifically, give your Cloud Run job's service account roles/bigquery.dataViewer and roles/bigquery.jobUser. No service account key file needed — Cloud Run uses attached identity.

Check it ran
gcloud run jobs executions list --job deloc-refresh-sales shows every run. Logs go to Cloud Logging; filter by resource.type="cloud_run_job".

End-to-end: BigQuery to dashboard, every hour

Putting it all together. You have a static dashboard (built with Vite / your choice) that reads data.json. You want it refreshed every hour from a BigQuery query.

1. Deploy the dashboard

shell
cd ./my-dashboard
npm run build
deloc deploy --name "sales-dash"
# → https://your-username.deloc.dev/sales-dash

Your dashboard reads ./data.json — for now it's empty or contains stub data. That's fine.

2. Generate the refresh pipeline

From an MCP-enabled editor, say:

text
"Set up a BigQuery refresh for sales-dash.
The query is: SELECT region, SUM(revenue) AS total
FROM sales.orders WHERE order_date >= CURRENT_DATE() - 30
GROUP BY region"

The setup_data_refresh tool writes main.py, requirements.txt, Dockerfile, and README.md into ./data-refresh/. Alternatively, create a Python project by hand using the source templates above.

3. Sanity-check locally

shell
cd data-refresh
pip install -r requirements.txt
export GCP_PROJECT=your-project
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/sa-key.json
export DELOC_TOKEN=dl_xxxxx
python main.py
# → Fetched 42 rows from bigquery
# → Pushed 42 rows to sales-dash (4,231 bytes)

Hit https://...deloc.dev/sales-dash/data.json in a browser — you should see the JSON your script just uploaded.

4. Deploy to Cloud Run

shell
# Authenticate & set project
gcloud auth login
gcloud config set project YOUR_PROJECT

# Create a service account for the job (once)
gcloud iam service-accounts create deloc-refresh
gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member "serviceAccount:deloc-refresh@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role "roles/bigquery.dataViewer"
gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member "serviceAccount:deloc-refresh@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role "roles/bigquery.jobUser"

# Build & push the image
docker build -t gcr.io/YOUR_PROJECT/deloc-refresh-sales .
docker push gcr.io/YOUR_PROJECT/deloc-refresh-sales

# Create the Cloud Run Job
gcloud run jobs create deloc-refresh-sales \
  --image gcr.io/YOUR_PROJECT/deloc-refresh-sales \
  --service-account deloc-refresh@YOUR_PROJECT.iam.gserviceaccount.com \
  --set-env-vars DELOC_TOKEN=dl_xxxxx,DELOC_APP_SLUG=sales-dash,GCP_PROJECT=YOUR_PROJECT
Use Secret Manager in production
For any real deployment, store DELOC_TOKEN in Google Secret Manager and mount it via --set-secrets DELOC_TOKEN=deloc-token:latest instead of --set-env-vars. Env vars are visible in the Cloud Console to anyone with run.jobs.get.

5. Schedule it hourly

shell
# Allow Cloud Scheduler to invoke the job
gcloud projects add-iam-policy-binding YOUR_PROJECT \
  --member "serviceAccount:deloc-scheduler@YOUR_PROJECT.iam.gserviceaccount.com" \
  --role "roles/run.invoker"

gcloud scheduler jobs create http deloc-refresh-sales-trigger \
  --location YOUR_REGION \
  --schedule "0 * * * *" \
  --uri "https://YOUR_REGION-run.googleapis.com/apis/run.googleapis.com/v1/namespaces/YOUR_PROJECT/jobs/deloc-refresh-sales:run" \
  --http-method POST \
  --oauth-service-account-email deloc-scheduler@YOUR_PROJECT.iam.gserviceaccount.com

6. Verify

shell
# Trigger the scheduler once, right now
gcloud scheduler jobs run deloc-refresh-sales-trigger --location YOUR_REGION

# Watch the execution
gcloud run jobs executions list --job deloc-refresh-sales

# Inspect app data
curl https://your-username.deloc.dev/sales-dash/data.json | jq .

That's the whole loop. New data every hour. Zero dashboard re-deploys.


macOS launchd (for dev machines)

If you want to run the refresh on your laptop while developing, launchd is cleaner than cron on macOS. setup_data_refresh doesn't emit a plist, but here's a working template:

~/Library/LaunchAgents/dev.deloc.refresh.sales.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>            <string>dev.deloc.refresh.sales</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/bin/env</string>
    <string>bash</string>
    <string>-c</string>
    <string>cd /Users/me/projects/deloc-refresh && DELOC_TOKEN=dl_xxxxx python main.py</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key> <integer>0</integer>
  </dict>
  <key>StandardOutPath</key>  <string>/tmp/deloc-refresh.log</string>
  <key>StandardErrorPath</key><string>/tmp/deloc-refresh.err</string>
</dict>
</plist>
shell
launchctl load ~/Library/LaunchAgents/dev.deloc.refresh.sales.plist
# Unload with:
launchctl unload ~/Library/LaunchAgents/dev.deloc.refresh.sales.plist
Check logs
Output goes to /tmp/deloc-refresh.log and /tmp/deloc-refresh.err. tail -f them after loading the agent to confirm the first run at the top of the hour.