I'm Zishan, a solo developer. I make a small Android arcade shooter called ZShoot under the name ZISHU TRON. When I finished version 3, I wanted more than a download link. I wanted a real home for the game: a place where players can try it, review it, ask for help and tell me what to build next.
The result is live at https://zshoot.zishuai.cloud/. This post is about how it is built, because the stack is small and you can copy it for your own project.
The goal
The site had to do five things:
- Look like an official game site, not a template.
- Let people download the APK and trust that the file is genuine.
- Let players leave reviews, send ideas and open support tickets.
- Give me a private admin panel to approve, reject and answer all of that.
- Be cheap to run and easy to update when I ship a new version.
I did not want a server to maintain, so I chose a static site on GitHub Pages and let Firebase do the dynamic parts.
The stack
-
Plain HTML, CSS and JavaScript. No framework, no bundler on the deployed site. Each page is a folder with an
index.html, so the URLs are clean:/feedback/,/ideas/,/ticket/,/about/,/admin/. - GitHub Pages for hosting, with a custom domain.
- Cloud Firestore for reviews, ideas, tickets, votes and site settings.
- Firebase Authentication with anonymous sign-in for players and Google sign-in for me.
I used a small Python script to assemble the pages from shared pieces (header, footer, sections), but what ships is just static files.
Players without accounts
Nobody wants to register to leave a review. So every visitor is signed in anonymously. Firebase gives each browser a random user ID, and everything a player sends is stored under that ID:
inbox/{uid} index document for the player
inbox/{uid}/items/{id} a review, an idea or a ticket
inbox/{uid}/items/{id}/msgs/{m} the conversation with me
pubReviews/{id} approved reviews (public)
pubIdeas/{id} approved ideas on the roadmap (public)
votes/{uid} one document per voter
site/release download link, version, checksum
A drawer called "My requests" reads only the player's own items. When I approve an idea, change its status or reply to a ticket, the player sees it there live. No email server needed.
Making myself the admin without hardcoding anything
My first version checked an email address that lived in a config file. That is easy to get wrong and it leaks into the repo. I replaced it with a document in Firestore:
- I open
/admin/and sign in with Google. - The page shows my UID.
- In the Firebase console I create
admins/<my-uid>with a fieldadmin: true.
The security rules then check for that document:
function isAdmin() {
return request.auth != null
&& request.auth.token.firebase.sign_in_provider != 'anonymous'
&& exists(/databases/$(database)/documents/admins/$(request.auth.uid));
}
match /admins/{uid} {
allow get: if request.auth != null && request.auth.uid == uid;
allow list, create, update, delete: if false;
}
Nobody can write to admins from the website, so nobody can promote themselves. Only I can, in the console.
Rules are the real security
The Firebase apiKey in a web app is public by design. What protects the data is the rules. A few that matter for this project:
- A player can create only their own items, with a fixed set of kinds and an allowed starting status, and with the admin decision fields untouched.
- A player can update only message bookkeeping fields on their own item, never the decision.
- Only the admin can read everyone's inbox and write
pubReviews,pubIdeasandsite. - Everyone can read approved content and site settings.
Here is the part that stops players from approving their own reviews:
allow update: if isAdmin() || (
request.auth.uid == uid
&& request.resource.data.diff(resource.data)
.affectedKeys().hasOnly(['lastMsgBy', 'lastMsgAt', 'updatedAt'])
);
I also added a honeypot field and a 30 second cooldown in the browser for basic spam control. Those are only speed bumps. The rules do the real work.
Updating the site without editing code
This was the feature I enjoy most. Every time I ship a new version I would have had to change the APK link, the version number, the file size, the SHA-256 checksum and the big "3" in the logo, on several pages.
Instead, those values live in one Firestore document, site/release, and the admin panel has a Release tab to edit them. Any element in the HTML can opt in with a data attribute:
<h1>ZShoot <span data-rel="major">3</span></h1>
<dd><span data-rel="version">3.0.0</span> (build <span data-rel="build">3</span>)</dd>
<a class="btn" href="#" data-apk>Download APK</a>
On load, the page applies cached values from localStorage immediately (so there is no flash), then fetches the live document from the Firestore REST API. Public pages do not need the Firebase SDK for this:
fetch('https://firestore.googleapis.com/v1/projects/' + projectId +
'/databases/(default)/documents/site/release?key=' + apiKey)
.then(r => r.ok ? r.json() : null)
.then(doc => { /* read stringValue fields, then fill every [data-rel] */ });
Values written into the page use textContent, and the APK link must start with https://, so a bad value cannot inject markup. If Firestore is unreachable, the hard-coded defaults in the HTML still show a working download button.
A playable demo in a canvas
The game is on Android, but I wanted visitors to feel it in the browser. The "Play" section is a small canvas game that uses the real crate and gun sprites. Crates fall, you tap them, and different crate types behave differently. It is not the full game, just enough to show the idea in ten seconds.
Small things that helped
- Trust for the APK. The download panel shows the file size, minimum Android version and a SHA-256 checksum with a copy button.
-
SEO basics. A
sitemap.xml, arobots.txtthat keeps/admin/out of search, and a canonical link on each page. - Privacy in plain language. The game itself needs no internet permission, so the Privacy Policy says exactly that, and describes what the website collects.
What I would do differently
- A static site cannot send email. Players see replies on the site instead. If I need real emails later, the Firebase "Trigger Email from Firestore" extension is the next step.
- I test rules last. I should have written rule tests from day one.
- I wrote the first version of the site before the second one, and had to merge the two. Deciding on the page structure first would have saved a day.
Try it
If you want to see the result, the site is here: https://zshoot.zishuai.cloud/. You can play the web demo, read the pages, and send me an idea or a review. I read every one.
If you are building something similar, I hope the structure helps. Questions are welcome in the comments.
Top comments (0)