FILIP FRLIĆ
ZAGREB--:--:--

Case study

Kvizapp

A general-knowledge quiz platform built end to end on my own — mobile app, Go backend, admin dashboard, marketing site, and a content pipeline that posts to Instagram by itself.

Role
Solo — design, mobile, backend, infrastructure
When
Personal product
Platforms
Android · Web
Highlight
7,000+ questions
  • React Native
  • Expo
  • Tamagui
  • TanStack Query
  • Go
  • Postgres
  • Firebase Auth
Kvizapp app screenshot

The brief

Kvizapp is a general-knowledge quiz app with a question base now past seven thousand entries. It started as a product I wanted to exist, which meant every layer of it was mine to design: the app people play, the API behind it, the tooling to author questions at that volume, and the marketing to get anyone to install it in the first place.

That last part shaped the architecture more than anything else. A quiz app lives or dies by a steady supply of content, and no solo project can sustain that by hand.

What I built

Five deployable pieces in one Turborepo monorepo: an Expo React Native app, a Go API server, a React admin dashboard for authoring questions, an Astro landing site, and a Node service that publishes to Instagram. The backend, dashboard and Instagram service run on Fly.io; the landing site is static on Cloudflare Pages.

The admin dashboard is built on react-admin, with a custom REST data provider that flattens the backend’s JSON:API-style payloads into the shape react-admin expects. Access is gated on an admin check against the backend rather than trusting the client.

A Go API sized for one maintainer

The backend is Go — chi for routing, GORM over Postgres, with the domain models and store interfaces sitting at the module root. There is deliberately no service layer and no package-level database global: handlers hang off a struct holding an injected store, which keeps the indirection low enough that one person can hold the whole thing in their head.

Every outbound third-party call goes through a single internal HTTP package, so calls share a connection pool and cannot be made without a timeout. That is the kind of constraint worth enforcing structurally rather than remembering.

Generating the mobile client instead of writing it

The app does not hand-write its API layer. The backend’s OpenAPI spec drives orval to emit typed fetchers, and a codegen step then emits one Legend-State observable per GET endpoint. Changing an endpoint regenerates the client, so a backend change that breaks the app breaks it at compile time rather than at runtime in someone’s hand.

The UI is Tailwind-style class names through Uniwind, with expo-router for file-based routing and TanStack Query alongside the generated layer.

An Instagram pipeline that feeds itself

The piece I would keep in any rebuild is the content automation. Cron jobs in the Go backend schedule one publish job per enabled channel, then call a Node service that renders the media — quiz videos assembled with ffmpeg, static cards with wkhtmltoimage — uploads it to the Instagram Graph API, polls the media container until it reports finished, and publishes.

Posts, reels and stories are one code path rather than three, with a channel value selecting the table. Three near-identical pipelines would have been the obvious way to build it and the wrong one to maintain.

Outcome

Kvizapp is live on Google Play with a question base past seven thousand, and the Instagram channel keeps publishing without me touching it. The more useful outcome was the shape of the thing: a full product surface — app, API, admin, marketing, growth — that one person can actually keep running.