A minimal, fast file uploader in the spirit of kappa.lol — drop a file, get a link, done.
frontend/— Astro (static output) + TypeScript. The UI: a drag-and-drop / paste uploader, upload progress, and a locally-remembered history of your uploads with copy-link and delete actions.backend/— Express + TypeScript API. Handles uploads (multer), stores file metadata in a small JSON store, and streams files back out.nginx/— a site config that serves the built frontend and reverse-proxies everything under/api/*to the backend using Nginx'slocationblock.
Yappa/
├── frontend/ Astro app → builds to frontend/dist (static)
├── backend/ Express API → builds to backend/dist
└── nginx/ yappa.conf → serves dist/ + proxies /api/* to the backend
The browser only ever calls relative paths like /api/upload. In production,
Nginx receives the request on port 80/443 and decides where it goes:
/api/*→ proxied to the Express app (127.0.0.1:3001by default)- everything else → served as static files from
frontend/dist
That's the whole trick — one origin from the browser's point of view, no CORS to think about, and the backend's actual port is never exposed publicly.
During local development there's no Nginx running, so astro dev proxies
/api/* to the backend itself (see frontend/astro.config.mjs), mirroring
the same behavior.
You need two terminals (or a process manager of your choice).
1. Backend
cd backend
cp .env.example .env # adjust if needed
npm install
npm run dev # http://localhost:6789, routes under /api/*2. Frontend
cd frontend
npm install
npm run dev # http://localhost:4566Open http://localhost:4321 — uploads go to http://localhost:4321/api/upload,
which Vite's dev proxy forwards to the backend on port 3001.
# Backend
cd backend
npm install
npm run build # → backend/dist
npm run start # or run with pm2 / systemd
# Frontend
cd frontend
npm install
npm run build # → frontend/dist (static HTML/CSS/JS)Deploy frontend/dist wherever nginx/yappa.conf's root points
(/var/www/yappa/frontend/dist by default — adjust to taste), run the
backend as a long-lived process (pm2, systemd, Docker, whatever you like) on
the port set in backend/.env, then enable the Nginx site:
sudo cp nginx/yappa.conf /etc/nginx/sites-available/yappa.conf
sudo ln -s /etc/nginx/sites-available/yappa.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginxUpdate server_name and PUBLIC_BASE_URL (in backend/.env) to your real
domain before going live.
All routes are mounted under /api.
| Method | Path | Description |
|---|---|---|
| GET | /api/health |
Liveness check |
| POST | /api/upload |
multipart/form-data with a file field |
| GET | /:id |
Stream the file back (id or id.ext) |
| GET | /api/info/:id |
JSON metadata for an upload |
| DELETE | /api/delete/:id?key= |
Delete an upload using the deleteKey from upload |
| GET | /api/stats |
Total file count and bytes stored |
POST /api/upload response:
{
"id": "aZ4kP9qL",
"name": "cat.png",
"size": 234821,
"mime": "image/png",
"url": "https://yappa.my.id/aZ4kP9qL.png",
"deleteUrl": "https://yappa.my.id/api/delete/aZ4kP9qL?key=...",
"uploadedAt": "2026-08-22T10:00:00.000Z"
}- Upload history lives only in
localStorageon the uploader's device — the backend doesn't track "who owns" a file beyond the one-time delete key returned at upload time. Keep that key (or the link) if you want to delete later. - Max upload size, storage paths, and the public base URL are all configured
via
backend/.env(seebackend/.env.example). - The SQLite "database" in
backend/data/yappa.dbis intentionally simple. Swapbackend/src/store.tsfor a switch to other database if you outgrow it.