2024

Product Shapers

The production site for Dawid Szkiełka's Shape Up practice — offer, case studies, training, and a CMS the content team can run without a deploy.

React 19VitePayload CMS 3Next.js 16PostgreSQLDocker ComposeCaddy
Product Shapers — laptop mockup

The product

Product Shapers is Dawid Jurand Szkiełka's consulting practice. He helps fintech, ecommerce, marketplace, and SaaS teams make roadmaps predictable — Shape Up cycles, continuous discovery, and change management (ADKAR) instead of another slide deck. I built the site and the CMS. I did not run the consulting.

The public site has to carry that practice: the offer (team ownership, discovery, delivery via Shape Up), the community around Product Cafe and FinTech Brew, a knowledge base, a twelve-week mentoring program, Shape Up trainings, and a form for a thirty-minute intro call. Case studies and articles — including the Useme story Dawid tells on the site — are meant to be edited by a content team, not by shipping a new frontend every time the copy changes.

Product Shapers case studies on a laptop

My role

Solo build: the Vite / React frontend, the Payload CMS schema, and both deployment paths — a single-VPS Docker Compose stack, and a Netlify + Render split.

How it's built

React 19 on Vite for the public site. Payload CMS 3 — itself a Next.js 16 app, not a standalone service — on PostgreSQL, with Drizzle as the ORM. The simpler deploy is Docker Compose with three services (Postgres, CMS, and a custom Caddy edge) behind one port. The other path puts the static frontend on Netlify and the CMS / database on Render.

Decisions

1. A resolver for Payload's server URL, not a hardcoded value

Payload assumes one serverURL for cookies, CORS, and admin asset URLs. In the Netlify + Render deploy the marketing host and the CMS admin are different hostnames, which silently breaks sessions. The fix is a precedence chain — resolvePayloadServerURL() — checking an explicit env override, a localhost case in development, an optional "prefer marketing host" flag, and a default-to-Render-if-hosts-differ fallback. The commit history shows this was found in production, across several iterations, not designed correctly on the first try.

2. SQLite as a disposable build-time database

On Render, the CMS build sets DATABASE_ADAPTER=sqlite at a throwaway file so next build can type-check and bundle without a live Postgres connection. It is never the runtime database.

3. Media storage is env-gated, not hardcoded

@payloadcms/storage-s3 only activates when S3-shaped env vars are present. That exists specifically to work around Render's free-tier ephemeral disk — uploads would otherwise vanish on every redeploy or cold start.

Product Shapers hiring and contact pages