Skip to content

Repository files navigation

Zellr

Paste a GitHub URL → get a live static site on its own subdomain.

https://{id}.zellr.nikhilwho.in

Turborepo Next.js Node.js TypeScript Redis Cloudflare R2

Zellr landing — paste a GitHub URL and get a live subdomain

Quote and footer

A public GitHub repo, built and served on its own subdomain:

Deployed Vite site on {id}.zellr.localhost:3001

Live URL in the browser


What is Zellr?

Zellr is a small Vercel-style static deploy platform. You submit a public GitHub repo; Zellr clones it, builds it (npm install && npm run build), stores the output in Cloudflare R2, and serves it on a unique subdomain.

The live product hostnames:

Hostname Service
zellr.nikhilwho.in Dashboard (Next.js web)
api.zellr.nikhilwho.in Upload API
{id}.zellr.nikhilwho.in Deployed site (request-handler)

Example: after a deploy with id 6g3xo, the site is at:

https://6g3xo.zellr.nikhilwho.in/

Locally (bypassing nginx) the same site is:

http://6g3xo.zellr.nikhilwho.in:3001/

or http://localhost:3001/ with Host: 6g3xo.zellr.nikhilwho.in.


How it works

 GitHub URL
     │
     ▼
 ┌─────────────┐   POST /upload     ┌──────────────────┐
 │  Web (:3000)│ ─────────────────▶ │ Upload service   │
 └─────────────┘                    │ (api :4000)      │
                                    └────────┬─────────┘
                                             │
                    1. generate 5-char id (e.g. 6g3xo)
                    2. git clone → upload source to R2
                       key prefix: output/{id}/
                    3. Redis LPUSH build-queue
                       HSET status {id} = uploaded
                                             │
                                             ▼
                                    ┌──────────────────┐
                                    │ Deploy service   │
                                    │ (background)     │
                                    └────────┬─────────┘
                                             │
                    4. BRPOP build-queue
                    5. download output/{id}/
                    6. npm install && npm run build
                    7. upload dist/ → R2 dist/{id}/
                    8. HSET status {id} = deployed
                                             │
                                             ▼
 ┌──────────────────┐   Host: 6g3xo.…     ┌──────────────────┐
 │ Browser request  │ ──────────────────▶ │ Request handler  │
 │ to subdomain     │                     │ (:3001)          │
 └──────────────────┘                     └────────┬─────────┘
                                                   │
                    id = hostname.split(".")[0]  →  6g3xo
                    GET R2 object dist/6g3xo/index.html
                    (or dist/6g3xo{path} for assets)

Status lifecycle

Redis status field Meaning
(missing) Unknown id
uploaded Source in R2, waiting for / in build
deployed Built assets in dist/{id}/, ready to serve

The landing page polls GET /status?id={id} every 2s until status is deployed, then shows https://{id}.{NEXT_PUBLIC_SITE_DOMAIN}.

How a subdomain request is resolved

For http://6g3xo.zellr.nikhilwho.in:3001/:

  1. DNS / nginx — *.zellr.nikhilwho.in points at the host; nginx matches the wildcard and proxies to 127.0.0.1:3001 (request-handler). Hitting :3001 directly skips nginx but uses the same handler.
  2. ID extraction — req.hostname is 6g3xo.zellr.nikhilwho.in → first label → 6g3xo.
  3. Path — / becomes /index.html.
  4. R2 fetch — object key dist/6g3xo/index.html.
  5. MIME — .html / .css / .js mapped; everything else application/octet-stream.

Assets like /assets/index-abc.js resolve to dist/6g3xo/assets/index-abc.js.

Build assumption: the repo must produce a dist/ folder (Vite / CRA-style). That folder is what gets uploaded and served.


Architecture

apps/
├── web/               # Next.js landing — submit repo, poll status
├── upload-service/    # Express API — clone, upload source, enqueue
├── deploy-service/    # Worker — build + upload dist
├── request-handler/   # Express — subdomain → R2 static files

Shared plumbing: Redis (build-queue list + status hash) and Cloudflare R2 (S3-compatible).


Deployment roadmap

The full plan for framework detection, static and dynamic build paths, R2, Docker/ECR/Lambda/ECS/Render options, Cloudflare wildcard DNS/TLS, reverse proxying, and GitHub Actions is in DEPLOYMENT_RUNBOOK.md.

Remaining work

  • Add a Postgres deployment registry and release state machine.
  • Implement framework presets, overrides, and fixture tests.
  • Move builds to an isolated runner.
  • Make R2 releases atomic, streamed, MIME-correct, and rollbackable.
  • Add dynamic Docker image build/push, health checks, and secret injection.
  • Replace first-label-only serving with registry-backed static/dynamic routing.
  • Deploy the control plane to Render and complete Cloudflare domain verification.
  • Add GitHub Actions CI plus one deploy trigger.
  • Add observability, audit logging, rate limits, validation, and abuse controls before public repository deployments.

Ports (Docker Compose)

Service Host bind Container
web 127.0.0.1:3000 :3000
upload-service 127.0.0.1:4000 :3000
request-handler 127.0.0.1:3001 :3001
deploy-service — worker only
redis internal :6379

Nginx (nginx/zellr.conf) fronts production:

  • zellr.nikhilwho.in → web :3000
  • api.zellr.nikhilwho.in → upload :4000
  • *.zellr.nikhilwho.in → request-handler :3001

Quick start

Redis is required. Use Docker for everything, or run Redis yourself and npm run dev.

Prerequisites

  • Node.js 18+
  • Cloudflare R2 credentials (S3 API token)
  • Either Docker or a local Redis (redis-server)

Configure

cp .env.example .env
# fill ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, R2_BUCKET_NAME

Local .env typically:

NEXT_PUBLIC_UPLOAD_API_URL=http://localhost:4000
NEXT_PUBLIC_SITE_DOMAIN=zellr.localhost
REDIS_URL=redis://127.0.0.1:6379

Option A — Docker (Redis + all services)

docker compose up --build

Compose starts Redis and the four apps. Open http://localhost:3000.

Option B — local Redis + Turborepo

Port 3001 must be free (request-handler). Then in two terminals:

redis-server
npm install
npm run dev

If upload/deploy crash with ECONNREFUSED 127.0.0.1:6379, Redis is not running.

Preview a deployed site locally

After status is deployed, map the id to loopback (macOS has no wildcard /etc/hosts) and open port 3001:

sudo sh -c 'echo "127.0.0.1 fv72y.zellr.localhost" >> /etc/hosts'
open "http://fv72y.zellr.localhost:3001/"

Or skip hosts:

curl -H "Host: fv72y.zellr.localhost" http://127.0.0.1:3001/

In production, *.zellr.nikhilwho.in DNS + nginx on 80/443 replaces /etc/hosts and :3001.

Useful Turbo filters:

npx turbo run dev --filter=web
npx turbo run dev --filter=upload-service

Root scripts: npm run build · npm run lint · npm run clean


API cheat sheet

Method Path Body / query Service
POST /upload { "repoUrl": "https://github.com/…" } upload-service
GET /status?id={id} — upload-service

POST /upload returns { message, id }. After deploy, open https://{id}.zellr.nikhilwho.in.

R2 key layout

Prefix Contents
output/{id}/ Cloned source (pre-build)
dist/{id}/ Built static assets (served)

Env vars

See .env.example.

Variable Used by Purpose
ACCESS_KEY_ID / SECRET_ACCESS_KEY / ENDPOINT / R2_BUCKET_NAME upload, deploy, request-handler R2 access
REDIS_URL upload, deploy Queue + status
NEXT_PUBLIC_UPLOAD_API_URL web (build-time) e.g. https://api.zellr.nikhilwho.in
NEXT_PUBLIC_SITE_DOMAIN web (build-time) e.g. zellr.nikhilwho.in

Per-service docs

About

No description, website, or topics provided.

Resources

Stars

22 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages