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.
  • TypeScript 93.4%
  • Dockerfile 5%
  • CSS 1%
  • HTML 0.6%
Find a file
niels a0bc7a1692
Merge pull request #5 from niels-emmer/docs-update
docs: sanity-check and update all docs for sessions 6–8
2026-02-27 17:58:10 +01:00
.claude feat: initial implementation of PWA Maker Android APK generator 2026-02-26 14:15:33 +01:00
backend Merge pull request #4 from niels-emmer/icon-fix 2026-02-27 17:44:05 +01:00
docs docs: sanity-check and update all docs for sessions 6–8 2026-02-27 17:53:43 +01:00
frontend Merge pull request #4 from niels-emmer/icon-fix 2026-02-27 17:44:05 +01:00
.env.example feat: add HMAC build token + honeypot to prevent bot abuse 2026-02-27 09:11:22 +01:00
.gitignore feat: initial implementation of PWA Maker Android APK generator 2026-02-26 14:15:33 +01:00
AGENTS.md docs: add AGENTS.md and CLAUDE.md for agent session bootstrapping 2026-02-27 17:56:36 +01:00
CLAUDE.md docs: add AGENTS.md and CLAUDE.md for agent session bootstrapping 2026-02-27 17:56:36 +01:00
docker-compose.prod.yml feat: initial implementation of PWA Maker Android APK generator 2026-02-26 14:15:33 +01:00
docker-compose.yml fix(docker): switch frontend to nginx-unprivileged to support cap_drop: ALL 2026-02-26 18:49:01 +01:00
LICENSE feat: initial implementation of PWA Maker Android APK generator 2026-02-26 14:15:33 +01:00
README.md docs: sanity-check and update all docs for sessions 6–8 2026-02-27 17:53:43 +01:00
SECURITY.md docs: sanity-check and update all docs for sessions 6–8 2026-02-27 17:53:43 +01:00
stats.md fix: flush SSE gzip buffer and guard onerror race condition 2026-02-27 17:42:41 +01:00

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

What it is

A self-hosted web application that wraps any HTTPS PWA in a Trusted Web Activity (TWA) 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

Desktop — full form

Mobile

Mobile view


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) — no manual icon prep required
  • Server-side APK build via 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

git clone https://github.com/niels-emmer/pwa-maker-android.git
cd pwa-maker-android

2. Configure

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

docker compose up -d --build

First build takes 1530 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=<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://<vps-ip>: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 ~25 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:

docker compose down -v

Development

Backend

cd backend
npm install
npm run dev        # tsx watch — hot reload
npm test           # vitest — 100 tests

Frontend

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 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 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, 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 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