https://github.com/niels-emmer/pwa-maker-android
Self-hosted web app that wraps any PWA in a Trusted Web Activity (TWA) and generates a signed Android APK for sideloading — no Android Studio required.
https://github.com/niels-emmer/pwa-maker-android
claude-code sonnet-4-6 vibecoding
Last synced: 3 months ago
JSON representation
Self-hosted web app that wraps any PWA in a Trusted Web Activity (TWA) and generates a signed Android APK for sideloading — no Android Studio required.
- Host: GitHub
- URL: https://github.com/niels-emmer/pwa-maker-android
- Owner: niels-emmer
- License: mit
- Created: 2026-02-26T13:15:41.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-02-26T18:31:30.000Z (4 months ago)
- Last Synced: 2026-02-26T19:58:31.432Z (4 months ago)
- Topics: claude-code, sonnet-4-6, vibecoding
- Language: TypeScript
- Homepage: https://github.com/niels-emmer/pwa-maker-android
- Size: 337 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Security: SECURITY.md
Awesome Lists containing this project
README
> [!CAUTION]
> **This entire project is the result of agentic coding.** It was built through prompts and iterative debugging sessions, with no human review of the code. It is to be taken as an experiment. Reasonable effort has been made to apply proper security architecture — SSRF protection, input validation, non-root containers, capability dropping, rate limiting — but the code has **not been audited by a human developer**. Run it on infrastructure you control, behind auth, at your own risk.
# PWA Maker — Android APK Generator
> Turn any Progressive Web App into a signed Android APK you can sideload directly onto any Android device. No Android Studio, no Play Store, no fuss.
**Live demo: [pwa.macjuu.com](https://pwa.macjuu.com)**
## What it is
A self-hosted web application that wraps any HTTPS PWA in a [Trusted Web Activity (TWA)](https://developer.chrome.com/docs/android/trusted-web-activity/) shell and produces a signed `.apk` file — ready to install directly on any Android phone.
Built with the same stack as a typical vibecoded PWA (React + Vite frontend, Express backend), so it runs consistently on both your MacBook and VPS.
---
## Screenshots
### Desktop

### Mobile

---
## Features
- Paste any HTTPS PWA URL → manifest fields auto-filled
- Configurable: app name, short name, package ID, theme/background colour, display mode, orientation, icon
- SVG icons auto-converted to 512×512 PNG (via [resvg-js](https://github.com/yisibl/resvg-js)) — no manual icon prep required
- Server-side APK build via [Bubblewrap](https://github.com/GoogleChromeLabs/bubblewrap) + Android SDK 36
- Live build log streamed via SSE while you wait (with keep-alive heartbeat)
- Download a signed APK directly in browser
- Dark theme, mobile-first UI
- Bot prevention: HMAC-signed build tokens (server-enforced) + honeypot field (client-side)
- Rate limiting, input validation, non-root container, no shell injection
- Docker Compose — one command deploy
- Production-hardened: nginx dynamic DNS, OOM-safe Gradle JVM cap, request logging
---
## Prerequisites
- Docker + Docker Compose on your VPS
- An SSL-terminating reverse proxy in front of the stack (Nginx Proxy Manager, Caddy, Traefik, etc.)
- That's it
---
## Quick start
### 1. Clone
```bash
git clone https://github.com/niels-emmer/pwa-maker-android.git
cd pwa-maker-android
```
### 2. Configure
```bash
cp .env.example .env
# Edit .env if you want to change rate limits or TTL
# All defaults are sane for a personal VPS
```
### 3. Build and start
```bash
docker compose up -d --build
```
> **First build takes 15–30 minutes** — the backend image installs JDK 17 + Android SDK (~1.5 GB). Subsequent builds use Docker layer cache and are fast.
> **Low-memory host?** The default Gradle JVM heap is capped at 512 MB, which is sufficient for a TWA build and leaves headroom on 4 GB machines. Override with `GRADLE_OPTS=-Xmx768m` in `.env` if you have more RAM to spare.
### 4. Point your reverse proxy at the app port
The frontend container binds to `HOST_PORT` on the host (default **8088**).
If a different port is free, set `HOST_PORT=` in `.env` before starting.
```
# .env
HOST_PORT=8088 # change to any free port on your host
```
Example Nginx Proxy Manager / Caddy upstream target: `http://:8088`
### 5. Open the app
Navigate to your domain. Paste a PWA URL, configure options, click **Generate APK**.
---
## Usage
1. **Enter your PWA URL** — e.g. `https://my-app.example.com`
- The manifest is fetched automatically and all fields are pre-filled
2. **Adjust options** — name, package ID, colours, orientation
3. **Click Generate APK** — watch the build log stream in real time (first build ~2–5 min while Gradle downloads dependencies; cached builds ~30s)
4. **Download** — click the Download APK button when the build completes
5. **Install** — transfer the `.apk` to your Android device and open it (enable "Install from unknown sources" in Settings → Security)
---
## Configuration
All configuration is via environment variables in `.env`:
| Variable | Default | Description |
|---|---|---|
| `HOST_PORT` | `8088` | Host port the frontend is exposed on |
| `NODE_ENV` | `production` | Node environment |
| `PORT` | `3001` | Backend listen port (internal) |
| `ANDROID_HOME` | `/opt/android-sdk` | Android SDK path (set in Docker) |
| `JAVA_HOME` | `/usr/lib/jvm/java-17-openjdk-amd64` | JDK path (set in Docker) |
| `GRADLE_USER_HOME` | `/home/appuser/.gradle` | Gradle cache (mounted as volume) |
| `GRADLE_OPTS` | `-Xmx512m -Xms128m` | Gradle JVM heap limits (safe for 4 GB hosts) |
| `MAX_CONCURRENT_BUILDS` | `3` | Max simultaneous APK builds |
| `BUILD_RATE_LIMIT_PER_HOUR` | `10` | Max builds per IP per hour |
| `BUILD_TTL_HOURS` | `1` | Hours to keep built APK available |
| `CORS_ORIGIN` | `*` | Allowed CORS origin |
| `BUILD_TOKEN_SECRET` | *(random)* | HMAC secret for build tokens — set with `openssl rand -hex 32`; if unset a random secret is generated per restart |
---
## Docker internals
```
pwa-maker-android/
├── frontend/ React + Vite SPA → served by Nginx
│ └── nginx.conf Nginx config: serves SPA + proxies /api/* to backend
├── backend/ Express + TypeScript + Android build toolchain
│ └── Dockerfile Node 20 + JDK 17 + Android SDK 36 (~1.5 GB image)
└── docker-compose.yml
```
The `gradle_cache` named volume persists between container restarts so Gradle dependencies (~200 MB) are only downloaded once.
To clear the Gradle cache:
```bash
docker compose down -v
```
---
## Development
### Backend
```bash
cd backend
npm install
npm run dev # tsx watch — hot reload
npm test # vitest — 100 tests
```
### Frontend
```bash
cd frontend
npm install
npm run dev # Vite dev server on :5200
npm test # vitest + React Testing Library — 43 tests
```
The frontend dev server proxies `/api/*` to `localhost:3001`.
---
## Authentication
This app has **no built-in auth**. Protect it at the reverse proxy level. See [SECURITY.md](SECURITY.md) for options and recommendations.
---
## Sideloading on Android
1. Enable **Install unknown apps** for your file manager / browser:
- Settings → Apps → Special app access → Install unknown apps
2. Transfer the `.apk` to your device (USB, email, cloud storage, local network)
3. Tap the file to install
The installed app will appear on your home screen / app drawer like any other app.
> Each build generates a fresh signing key. If you reinstall a newer build of the same app, you must first uninstall the old version (Android enforces consistent signing for upgrades). This is acceptable for personal sideloaded use.
---
## Security
See [SECURITY.md](SECURITY.md) for the full security design, reporting policy, and hardening notes.
---
## Tech stack
| Layer | Technology |
|---|---|
| Frontend | React 18, Vite, TypeScript, Tailwind CSS |
| Backend | Node 20, Express, TypeScript |
| APK generation | [@bubblewrap/core](https://github.com/GoogleChromeLabs/bubblewrap), Android SDK 36, JDK 17 |
| Build tooling | Gradle (Android) |
| Signing | `apksigner` (Android build tools) |
| Progress delivery | Server-Sent Events (SSE) |
| Tests | Vitest, React Testing Library |
| Container | Docker, Nginx |
---
## Credits
### [Bubblewrap](https://github.com/GoogleChromeLabs/bubblewrap) by Google Chrome Labs
The APK generation pipeline depends on `@bubblewrap/core` as an npm library. A single call to `TwaGenerator.createTwaProject()` generates the Android project structure, `build.gradle`, and `gradlew` inside a temp directory. None of the bubblewrap source code is copied into this repository — it is used strictly as a published npm package.
Everything else in this project (Express server, manifest fetching, SSRF protection, rate limiting, SSE progress streaming, signing pipeline, React frontend, Docker setup) is original code.
---
## License
MIT