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


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 extractionreq.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
└── doc/               # Product docs site (optional, not in compose)

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

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

Prerequisites

  • Node.js 18+
  • Docker (recommended) or a local Redis
  • Cloudflare R2 credentials (S3 API token)

Configure

cp .env.example .env
# fill ACCESS_KEY_ID, SECRET_ACCESS_KEY, ENDPOINT, R2_BUCKET_NAME
# set NEXT_PUBLIC_UPLOAD_API_URL / NEXT_PUBLIC_SITE_DOMAIN for your env

Run with Docker

docker compose up --build

Preview a deployed site locally (hostname)

Browsers need DNS. On your Mac there is no real *.zellr.nikhilwho.in yet, so map the deploy id to localhost with /etc/hosts, then open port 3001 (request-handler; no nginx locally).

# after deploy status is "deployed" (replace ohu8a with your id)
curl "http://127.0.0.1:4000/status?id=ohu8a"

# fake DNS for that subdomain only (macOS has no wildcard /etc/hosts)
sudo sh -c 'echo "127.0.0.1 ohu8a.zellr.nikhilwho.in" >> /etc/hosts'

# open in browser
open "http://ohu8a.zellr.nikhilwho.in:3001/"

Or skip hosts and use curl:

curl -H "Host: ohu8a.zellr.nikhilwho.in" http://127.0.0.1:3001/

In production, one DNS record *.zellr.nikhilwho.in → EC2 replaces /etc/hosts, and nginx serves on 80/443 (no :3001 in the URL).

Run locally (Turborepo)

redis-server          # separate terminal
npm install
npm run dev           # all apps via Turbo

Useful 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

Watchers

Forks

Releases

Packages

Contributors

Languages