DEV Community

Cover image for I built the official website for my solo Android game with plain JavaScript, GitHub Pages and Firebase
Zishu Tron
Zishu Tron

Posted on

I built the official website for my solo Android game with plain JavaScript, GitHub Pages and Firebase

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:

  1. Look like an official game site, not a template.
  2. Let people download the APK and trust that the file is genuine.
  3. Let players leave reviews, send ideas and open support tickets.
  4. Give me a private admin panel to approve, reject and answer all of that.
  5. 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. I open /admin/ and sign in with Google.
  2. The page shows my UID.
  3. In the Firebase console I create admins/<my-uid> with a field admin: 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;
}
Enter fullscreen mode Exit fullscreen mode

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, pubIdeas and site.
  • 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'])
);
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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] */ });
Enter fullscreen mode Exit fullscreen mode

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, a robots.txt that 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)