VibeHost

Deploy HTML reports on a schedule

Deploy cron-driven HTML reports to a permanent URL that always shows the latest data, instead of sharing them through Notion, Google Sheets, or Tableau.

Your data pipeline generates an HTML report with charts, tables, and some narrative. Deploy it to a stable VibeHost URL on a schedule. Stakeholders bookmark it once, and the URL always shows the latest data.

In practice a growth team might run a fleet of date-stamped report apps (report-20260521-k7qz, report-20260520-a7k2, ...) alongside a couple of rolling apps (growth-daily, kpi-2026).

Two patterns

PatternURLUse when
Rolling (same URL, latest data)https://growth-daily-acme.vibehost.spaceStakeholders want a bookmark that's always current
Date-stamped (new app each run)https://gr-report-20260521-070012-acme.vibehost.spaceYou need historical snapshots and want each URL to be permanent

Every app URL ends in the workspace slug, acme in these examples.

Rolling is simpler. Date-stamped gives you a permanent record of what the report said on each day, without relying on rollback.

Rolling pattern

Your pipeline produces report-output/index.html and its assets every morning. Deploy that directory:

vibehost deploy ./report-output --app growth-daily

The bookmark https://growth-daily-acme.vibehost.space always shows the latest deploy.

Wrap it in your cron job or scheduler:

#!/usr/bin/env bash
# /opt/reports/daily-deploy.sh
set -euo pipefail

cd /opt/reports
export VIBEHOST_TOKEN="$(cat /run/secrets/vibehost_pat)"

# 1. Run the pipeline
python3 generate.py --date "$(date +%Y-%m-%d)" --out ./report-output

# 2. Deploy, and fail unless the platform reports the deployment healthy
LOG="/var/log/deploys/$(date +%F).json"
vibehost deploy ./report-output --app growth-daily --json > "$LOG"
jq -e '.data.status == "healthy"' "$LOG" > /dev/null

URL="https://growth-daily-acme.vibehost.space"
echo "[$(date -u +%FT%TZ)] deployed: $URL" >> /var/log/deploys/history.log

The script doesn't request the URL to check the deploy. A report is usually workspace-only (see Access control), and a gated app answers an anonymous request with a redirect to sign in, so curl -f on the URL passes whether or not today's report is live. healthy confirms that the deployment succeeded. For a public report, the smoke check in CI/CD with GitHub Actions also confirms that the URL serves it.

Then schedule it with a systemd timer:

[Unit]
Description=Daily growth report deploy
[Timer]
OnCalendar=*-*-* 07:00:00
Persistent=true
[Install]
WantedBy=timers.target
[Unit]
Description=Daily growth report
[Service]
Type=oneshot
ExecStart=/opt/reports/daily-deploy.sh
User=reports
StandardOutput=journal
StandardError=journal

Date-stamped pattern

APP="gr-report-$(date +%Y%m%d-%H%M%S)"

vibehost app create "$APP"
vibehost deploy ./report-output --app "$APP"
vibehost app visibility "$APP" workspace

Each run gets a new app. The name includes the time down to the second, so rerunning the job on the same day creates another app instead of deploying over the morning's snapshot. A snapshot's URL keeps working as long as its app exists and nothing else is deployed to it, so a link to the report from May 21, 2026 keeps working.

The upside is that each snapshot has its own permanent URL, and comparing today with yesterday takes two browser tabs.

The downsides:

  • Apps pile up. The free tier caps a workspace at 100 apps, so you'll need to vibehost app delete old ones now and then, or switch to the hybrid pattern below. Deleting an app takes its URL down, and every link to that snapshot stops working, so download any report you still need (vibehost pull --app <app>) before you delete it.
  • The bookmark changes every day. Stakeholders need an index page that links to today's report.

Deploy to a rolling app so the bookmark stays current, and record each deploy's deployment ID in your wiki or sheet as the archive:

DEPLOY_RESP=$(vibehost deploy ./report-output --app growth-daily --json)
DEPLOYMENT_ID=$(echo "$DEPLOY_RESP" | jq -r '.data.id')

echo "$(date -u +%F): $DEPLOYMENT_ID" >> /opt/reports/archive-ids.txt

Stakeholders bookmark growth-daily-acme.vibehost.space. When someone needs the May 21 report in particular, look up its ID in the archive and download that day's files:

vibehost pull --app growth-daily --deployment <id>

You keep one app instead of one per day. The archive holds IDs rather than links, so an old report opens from the pulled files and has no URL of its own. If stakeholders need to open past reports in a browser themselves, use the date-stamped pattern.

vibehost gc keeps only the most recent deployments per channel (5 by default). Once someone runs it, older IDs in the archive stop working, so pull any day you want to keep before then.

CI variant

.github/workflows/daily-report.yml
name: Daily report
on:
  schedule:
    - cron: '0 7 * * *'   # 07:00 UTC daily
  workflow_dispatch:

jobs:
  report:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v6
        with: { python-version: '3.12' }
      - run: pip install -r requirements.txt
      - run: python generate.py --out ./report-output
      - run: curl -fsSL -o vibehost-install.sh https://vibehost.com/install.sh && sh vibehost-install.sh && rm vibehost-install.sh
      - env:
          VIBEHOST_TOKEN: ${{ secrets.VIBEHOST_TOKEN }}
        run: ~/.vibehost/cli/vibehost deploy ./report-output --app growth-daily

Compared with a self-hosted cron, there's no server to maintain, the logs live in GitHub Actions, and you can re-run any day on demand.

The catch is data egress on GitHub-hosted runners. If your pipeline pulls 10GB of raw data every day, a larger self-hosted machine will run it faster.

Access control

Reports usually shouldn't be public. Make the app workspace-only, so everyone signed into your workspace can view it, or invite specific stakeholders by email. People outside your workspace sign in with Google:

vibehost app visibility growth-daily workspace

vibehost app grants add-email vp@acme.com viewer --app growth-daily

vibehost app grants add-email partner@vc.com viewer --app growth-daily

For external review without sign-up, use a share link.

Why not Notion / Sheets / a BI tool

SheetsNotionTableauVibeHost report
Interactive chartsLimitedNoYesAny JS lib
Mobile layoutBrokenPassableSlowYour HTML
Load timeSlow at scaleSlow at scaleSlowStatic HTML
BrandingGoogleNotionTableauYours
Auth modelGoogle sharingNotion sharingLicense-basedVibeHost grants
Embed in emailNoLimitedNoiframe-friendly
Cost$$$$$$$$

If your data team already has a BI tool, that's probably the right answer. VibeHost reports fit when you want full control over how the report looks (your chart library, your layout, your branding) and nothing to run behind it, with no server or database connection to manage.

See also

On this page