← All workCase study · PWA + CMS

GUT Music

An installable app for artists and a CMS for the staff, both on the same backend API. Releases are checked against the QA checklist with end-to-end tests.

Role
Frontend lead
Timeline
Jun 2025 – now
Team
Separate backend team, product and design
Live
gutmusic.media

Problem

GUT Music is a platform for emerging artists. Artists build a profile, upload reels and apply to auditions. They also get a wallet with vouchers and can buy premium services from GUT and its partners. Artists, fans and the public can browse artists, auditions, reels, events and collections. The staff approves new artists and manages the content from a CMS.

Both apps run on an API written by a separate backend team. A change in a response could break a screen with no warning. Before each release, the QA checklist was checked by hand, and that took a full day.

Architecture

The artist app and the CMS are two React clients. Every request goes through a contract layer of Zod schemas and a typed client, and then to the backend API. The end-to-end tests use the same schemas. Woodpecker CI builds and deploys both apps. The end-to-end suite runs against staging before a release.

Artist PWA and CMS call the backend API through a shared Zod contract layer. Woodpecker CI builds and deploys to staging, where Vitest and Playwright check each release.Artist PWAReact · Vite · Workboxpersisted query cacheEditorial CMSReact · TanStack TableReact Hook FormContract layerZod schemas · typed clientone source for types + e2eBackend APIRESTbackend teamDatabackend teamHTTPSVitest · Playwrightunit + e2e, linked to QA rowsStagingchecked before each releaseWoodpecker CIbuild and deployrunsdeployguards releasesbuilt by meowned by other teams
I own both clients, the contract layer and the test suites. The tests run against staging before a release; the CI only builds and deploys. I read the API code to agree on contracts with the backend team, but I don't change it.

I read the API code to agree on contracts with the backend team, but I don’t change it.

Scope

Mine

  • The artist PWA and the CMS, from routing to forms
  • The typed API client, with runtime validation on every response
  • Instant reopen and offline reads through a persisted query cache
  • Unit and end-to-end suites, and the link between the tests and the QA checklist

Other teams

  • Backend API and data model (backend team)
  • Product decisions and visual design (product and design)
  • Final sign-off of a release, still done by hand on the checklist

Key decisions

Validate every API response at runtime

Why. The API belongs to another team. When a contract changes, the Zod error names the field that changed, and the screen shows an error state instead of broken data.

Trade-off. The schemas need upkeep. In return they are the only source of types, for the apps and for the tests.

Persist the query cache in the PWA

Why. Artists open the app on their phone, often on a weak connection. The cache is saved to storage, so the last data shows up at once, also offline.

Trade-off. The cache has to be cleared on logout and on account switch. Queries with private or short-lived data opt out of persistence one by one.

Why. Each row of the checklist has a test or is marked as manual. A test run gives a pass or a fail for every row, in the terms QA already uses.

Trade-off. When the checklist is renumbered, the links have to follow. A script remaps them.

In code

The contract layer, simplified:

reels.tsillustrative
const Reel = z.object({
  uuid: z.string(),
  title: z.string(),
  status: z.enum(["draft", "pending", "published"]),
  createdAt: z.string(),
});

export type Reel = z.infer<typeof Reel>;

// A changed field fails here, with its path, not in the UI
export const useArtistReels = (artistId: string) =>
  useQuery({
    queryKey: ["artists", artistId, "reels"],
    queryFn: async () =>
      z.array(Reel).parse(await api.get(`/artists/${artistId}/reels`)),
  });

Results

1 day → 1 h
to check every use case in the QA checklist before a release
128
end-to-end tests, about 107 linked to rows of the QA checklist
~1,160
unit tests across the app and the CMS

A release is still signed off by hand. The difference is on the tech side. A test run of about an hour now shows, in the same way every time, that all the use cases in the checklist pass. The same check used to take a full day.