---
name: ship-an-app-free
description: >-
Take an idea to a working app at a public URL using only free tiers — GitHub Pages for the site, GitHub Actions for the build, and a free serverless backend when the app needs one. Written for someone who has never deployed anything: what to choose, the exact commands, the settings that silently break a deploy, and how to verify the live URL actually serves your app.
---
# ship-an-app-free — idea → public URL, $0, no server to rent
You do not need a credit card, a VPS, or a devops background. You need a GitHub account and an
agent that can run commands.
## Choose the shape first — this decides everything after
| Your app | Host | Cost |
|---|---|---|
| Site, docs, portfolio, dashboard, calculator, game — **runs entirely in the browser** | **GitHub Pages** | free forever |
| Needs a server: logins, a database, secret API keys, webhooks | **Cloudflare Workers** free tier | free to 100k req/day |
| Needs to run on a schedule | **GitHub Actions** cron | free for public repos |
| Needs a real database | **Cloudflare D1** or **Turso** free tier | free tier |
> **Start in the browser if you possibly can.** A static app has no server to secure, no bill
> to run up, no cold starts, and no way to leak a secret key. Most "I need a backend" ideas
> don't — until they need logins or a private API key.
**A secret in browser code is public.** Anyone can read it with View Source. If your app needs
an API key, that is exactly the moment it needs a backend — jump to the Workers section.
---
## Path A — static app on GitHub Pages
### 1. Make the repo
```bash
mkdir my-app && cd my-app
git init
echo "
hello
" > index.html
git add -A && git commit -m "first commit"
gh repo create my-app --public --source=. --push
```
No `gh`? Install the [GitHub CLI](https://cli.github.com/), or create the repo in the web UI
and `git push` to it.
### 2. Turn Pages on
```bash
gh api -X POST repos/{owner}/my-app/pages -f build_type=workflow
```
Or in the web UI: **Settings → Pages → Source: GitHub Actions**.
### 3. Add the deploy workflow
`.github/workflows/deploy.yml` — this exact file works for a plain HTML/CSS/JS app:
```yaml
name: Deploy to Pages
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: . # for a built app use ./dist or ./build
- id: deployment
uses: actions/deploy-pages@v4
```
**The `permissions:` block is not optional.** Without `pages: write` and `id-token: write` the
deploy fails with a permissions error that reads like an account problem but is just this.
For a framework app (React/Vite/Next static export), build first and upload the output dir:
```yaml
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci && npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: ./dist # Vite: dist · CRA: build · Next export: out
```
### 4. Deploy and verify
```bash
git add -A && git commit -m "add deploy workflow" && git push
gh run watch # follow the build
gh api repos/{owner}/my-app/pages --jq .html_url # your live URL
```
**Verify it serves YOUR app, not a 404 page:**
```bash
curl -sSL -o /dev/null -w "%{http_code}\n" "$(gh api repos/{owner}/my-app/pages --jq .html_url)"
curl -sSL "$(gh api repos/{owner}/my-app/pages --jq .html_url)" | head -20
```
A `200` alone is not proof — GitHub's 404 page is itself served with a 200 in some paths.
**Read the HTML and confirm it's yours.**
### The four things that break a Pages deploy
| Symptom | Cause | Fix |
|---|---|---|
| 404 at the URL, workflow green | uploaded the wrong `path:` | point it at the real build output |
| Blank page, console 404s on `/assets/…` | app assumes it's at the domain root; Pages serves at `/repo-name/` | set the base path — Vite: `base: '/my-app/'`; Next: `basePath` |
| Deploy fails on permissions | missing `permissions:` block | add all three lines |
| Pushes don't redeploy | workflow watches the wrong branch | match `branches:` to your default branch |
| 404 on client routes like `/app/dashboard` | SPA client-side routing fallback missing | after build: add `touch out/.nojekyll && cp out/index.html out/404.html` (Vite: `dist/`; CRA: `build/`; Next: `out/`) |
---
## Path B — needs a backend: Cloudflare Workers
Use this when you have logins, a private API key, a database, or webhooks.
```bash
npm install -g wrangler
wrangler login
wrangler init my-api
cd my-api
wrangler deploy
```
You get a public `*.workers.dev` URL immediately. Keep secrets **out of the code**:
```bash
wrangler secret put MY_API_KEY # prompts; never appears in the repo
```
Read it in the Worker as `env.MY_API_KEY`. **Never** put a key in a `.js` file, in
`wrangler.toml`, or in a GitHub Actions log.
Wire your Pages frontend to it by calling the Worker URL, and allow that origin in the
Worker's CORS headers. The **[`website-as-code`](website-as-code.md)** and
**[`repo-to-website`](repo-to-website.md)** skills in this pack cover the site side in depth.
---
## Path C — runs on a schedule
Free cron, no server, in `.github/workflows/cron.yml`:
```yaml
name: scheduled job
on:
schedule:
- cron: '0 9 * * *' # 09:00 UTC daily — UTC, not your timezone
workflow_dispatch: # keep this: lets you run it manually to test
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./my-script.sh
```
**Always include `workflow_dispatch`** — otherwise your only way to test a daily job is to
wait a day. Scheduled runs on free accounts can be delayed by several minutes under load, and
GitHub disables schedules on repos with no activity for 60 days.
---
## Let your agent build it
You have an agent and a local model. Use them:
> "Build a single-page app that does X. Plain HTML, CSS, and JavaScript, no build step.
> Then follow the ship-an-app-free skill to deploy it to GitHub Pages and give me the live URL."
Then hold it to the verification step — **make it show you the fetched HTML from the live
URL**, not just a green checkmark. A green workflow that deployed the wrong directory is the
most common outcome, and it looks like success everywhere except the actual page.
## Free-tier limits worth knowing before you build
| Service | Free tier | Where it bites |
|---|---|---|
| GitHub Pages | 1 GB site, 100 GB/mo bandwidth, ~10 builds/hr | soft limits; fine for real projects |
| GitHub Actions | unlimited for public repos; 2,000 min/mo private | make the repo public if you can |
| Cloudflare Workers | 100k requests/day | plenty for a side project |
| Cloudflare D1 | 5 GB storage | plenty for a side project |
**Public repos get dramatically more free compute than private ones.** If the code isn't
sensitive, make it public — and run [`secretguard`](secretguard.md) over it first to be sure
nothing private is committed.
## Next
- **[`secretguard`](secretguard.md)** — scan for leaked secrets **before** going public
- **[`security-audit`](security-audit.md)** — OWASP pass once it's live
- **[`repo-to-website`](repo-to-website.md)** / **[`website-as-code`](website-as-code.md)** — richer site builds
- **[`aither-start`](aither-start.md)** — the guided path this plugs into