HTML5 game publishing

Building PlayKovo with Supabase: From HTML5 Games to a Distribution Platform

How we use Supabase PostgreSQL, Authentication, Row Level Security and database functions to power our game portal, developer tools and partner distribution network.

PlayKovo Team7 min read
Games currently available on PlayKovo. Open a title to see its current listing.

PlayKovo is a place to play HTML5 games in the browser, with no download and no sign-up. Today the catalog holds 767 published games across 34 categories. Around that catalog sit three groups of people: players, game developers who submit and update their own games, and website owners who embed our games on their own pages.

All three run on one Supabase project and a Next.js app, built and maintained by a small team. This post walks through how that works: accounts, the developer portal, honest statistics, partner distribution and the mobile app, and the design principles that hold it together.

Why Supabase

Our data is relational. A game belongs to categories, has tags, comes from a developer, collects play sessions and ratings, and can be embedded on many domains. We wanted plain PostgreSQL with joins, constraints and indexes rather than a document store we would have to work around later.

We also needed accounts, a safe way to read data from both the browser and the server, and a place for logic that should run next to the data. Supabase gives us PostgreSQL, Authentication, Row Level Security and database functions in one project, so the rule about who can read a row lives next to the row instead of being repeated in every page.

The schema lives in SQL migrations in our own repository, which keeps the data model portable and reviewable like any other code.

How the pieces fit

The site is a Next.js app. Supabase holds data, accounts and access rules. Game builds and images live in Cloudflare R2 behind a CDN, so large files never pass through the database; Postgres stores only where each file is.

Players, developers and partner websites reach a Next.js app, which uses Supabase for data and accounts and Cloudflare R2 with a CDN for game files and images
Next.js in front, Supabase for data and accounts, R2 and a CDN for game files.

Games run in an iframe on a separate origin from the main site, so third-party game code never shares cookies with PlayKovo. When a game needs something from the player's account, such as a cloud save, it asks the parent page through our SDK and the page does the work with the real session.

One account system for players, developers and staff

Everyone signs in through Supabase Auth, with email and password or with Google. A player account and a developer account are the same kind of user; what changes is the profile attached to it and the parts of the site it unlocks. A role on the profile decides who reaches the review tools.

Staff accounts go one step further. Supabase Auth supports multi-factor authentication with an authenticator app, and every session carries an assurance level. Review pages check on the server that the session has completed the second factor before anything loads, so a stolen password alone is not enough to reach them.

The developer portal

Developers submit games through a five-step flow: details, build, store art, a preview and QA check, and the final submission. Each step saves to the same draft, so nothing is lost if they close the tab halfway.

  • Uploads go straight to storage. The browser receives a short-lived signed URL and uploads the ZIP or artwork directly to R2. The server then checks the file before it becomes part of the submission. Large builds never pass through a serverless function.
  • Review is a status, not an email thread. A submission moves through draft, submitted, in review, changes requested, approved and published. Every change is recorded in a history table, and the developer gets an email at each step that matters.
  • Live games can be updated safely. When a developer updates a published game, the live version keeps playing while the update is reviewed. Only approval switches players to the new build.
  • Each developer sees only their own data. Row Level Security does this in the database, not in page code.

The core of that last point is a short policy:

alter table public.submissions enable row level security;

create policy "owners read their own submissions"
  on public.submissions
  for select to authenticated
  using (user_id = auth.uid());

If a dashboard query ever asks for the wrong row, the database simply returns nothing.

Statistics developers can trust

Developers see views, clicks, plays and play time for their games. Those events start in browsers and inside games, so we treat them as untrusted input and design the write path around that.

  • Writes go through the server. The browser asks our server to record an event. The server checks that the game exists and is published, then writes through a database function that runs with the right permissions.
  • The database refuses duplicates. Retries and refreshes can send the same event twice. Events that games send, such as ad events, carry a request id, and a unique constraint makes the second insert harmless.
  • Two shapes for two jobs. Each view is stored once as a daily counter that dashboards read instantly, and once as a timestamped event row for hourly charts.

Distributing games to partner websites

PlayKovo games do not only run on PlayKovo. Website owners can add our games to their own sites, and 134 games are currently offered in the public embed catalog.

  • A public catalog feed. Partners pull the list of embeddable games as JSON or XML. It is built from the same tables as our own pages and cached for five minutes, so a busy partner site does not turn into a busy database.
  • Opt-in per game. A flag on each game decides whether it is offered for embedding, and developer-submitted games are only offered when the developer has agreed to catalog distribution.
  • Embed pages built for other people's iframes. The embed page adapts to whatever frame size the host page gives it, from a small sidebar box to a full-width player.
  • Partner sites and traffic per domain. Partner domains are stored with an approval status, and play sessions record which domain they came from. That value is set by the server, never by the browser, because anything a browser sets can be faked.

None of this needed a separate service. It is a few tables, policies and functions on the same Postgres database, plus one cached read-only endpoint.

Cloud saves that follow the player

Signed-in players get progress that follows them across devices. There is one row per player and game, and a single policy lets each player read and write only their own row:

create policy "players manage their own saves"
  on public.game_saves
  for all to authenticated
  using (auth.uid() = user_id)
  with check (auth.uid() = user_id);

Games call the PlayKovo SDK to save and load. Because the game runs on its own origin, the SDK passes the request to the PlayKovo page, which writes it with the player's real session. Save size is limited in the database itself, so no single game can grow a player's storage without bound.

The PlayKovo mobile app

We are building a PlayKovo app in Flutter. It does not have its own copy of the catalog. It reads the same games, the same artwork and the same rules as the website, and a game appears in the app only after it has been approved for mobile. 104 games are enabled for mobile so far.

The app reports plays and sessions to the same backend as the website, so app and web numbers live in one database instead of two separate systems. Building a new client on top of the existing schema, rather than a new backend, is one of the main reasons the data model lives in Postgres in the first place.

Migrations as the source of truth

Every database change is a SQL file in git, named with a timestamp so the order is never in doubt. The folder holds 74 of them, and nothing is changed by hand in the dashboard. A few habits keep that manageable:

  • Access rules ship with the table. Row Level Security and its policies go in the same migration that creates the table.
  • Migrations are safe to run twice. if not exists, drop policy if exists and guarded do blocks make a second run harmless.
  • The reason goes at the top. A short comment explaining why the change exists is worth more than the SQL six months later.
  • Applied migrations are never edited. A change always goes into a new file.

Design principles

  • Rows, columns and functions each get their own rule. Row Level Security decides which rows a user can see. Column privileges decide which fields of those rows are exposed. Database functions get explicit execute grants. They are separate mechanisms in PostgreSQL, and we set all three on purpose.
  • The server key stays on the server. The Supabase service role is used only in server code, for work that has already passed our own checks.
  • Never trust a number from the browser. Game ids, domains and event counts are checked on the server or in SQL.
  • Precompute what dashboards read. Daily counters keep dashboards fast; event tables are kept for detail.
  • Cache on purpose. Catalog pages and feeds are cached for a few minutes. Changes take a moment to appear, and in return traffic spikes never reach the database directly.
  • Code before permissions. When access rules change, the code that relies on the new rules ships first, and the database change follows.

What's next

Next on the list are the mobile app launch, richer statistics for developers, and more tools for partner sites. All of it builds on the same Supabase project described here. If you are building something similar, our short advice is: keep the schema in migrations, give every table, column and function a deliberate rule, and let the database enforce what matters most.

If you build HTML5 games and want them in front of more players, see the PlayKovo developer page.

We use optional cookies for site analytics and site advertising. You can change your choice later. Privacy policy.