The wallpaper press

A small digital storefront I built for selling hand-cut wallpapers, one image or one pack at a time. The brief I gave myself was simple: it should
feel like a quarterly print magazine that happens to live on the internet, not another SaaS landing page. The build is Next.js 15 on the App Router,
Supabase for data and auth, Stripe for checkout, and Netlify for hosting.

What it does

Buyers can browse a catalog, pick up a single wallpaper for a few dollars, or buy a curated pack and get every wallpaper inside it. A pack purchase
implicitly entitles the buyer to every wallpaper it contains, so if you later wander into the detail page for one of those wallpapers you already own
through the pack, the button quietly turns into Download. No double-charging, no awkward overlap. Buyers download via 10-minute signed URLs minted on
demand — the wallpapers bucket itself is private and nobody ever gets a direct path to the file.

The pieces I’m proudest of

Two-layer pricing. There are two price tokens in a Supabase app_settings table: the default wallpaper price and the default pack price, both editable
from /admin/settings. Every item can optionally override the token on its own row. If the override is null, the token wins. If the override is lower
than the token, the UI automatically renders it as a sale — discounted price next to the struck-through token. If it’s higher, it reads as a premium
item. So a sitewide repricing is one form submission, and individual sales are one number on one row. No code change, no deploy.

Six-digit code auth, no links. I deliberately skipped magic-link sign-in. Corporate email scanners trigger the link before the user does, which
silently burns the one-time token and locks people out. Supabase’s email OTP issues a {{ .Token }} you can drop straight into a template, so the user
reads a six-digit code from their inbox and types it on the site. No PKCE, no redirect URLs to maintain across preview deploys, no scanner-burn. Both
the “Magic Link” and “Confirm signup” templates ship the code, because Supabase routes new and returning users through different templates and either
one needs to land on a code.

A single entitlements view. user_wallpaper_entitlements is a Postgres view that unions direct wallpaper purchases with the wallpapers a buyer
indirectly owns through a pack. It runs with security_invoker = true so RLS still applies as the calling user, and every download endpoint, every
Download button, every “you already own this” check reads from that one view. The unioned-ownership rule lives in one place; nothing in the app has to
remember it.

Hard-edged data integrity. The purchases table has a CHECK constraint called purchases_exactly_one_item that requires every row to point at either a
wallpaper or a pack, never both, never neither. The Stripe webhook writes whichever one the checkout was for. That single constraint means downstream
queries never have to defend against a “purchase of nothing” or a “purchase of two things at once” — the database refuses to accept either.

Manual newsletter trigger. The first version auto-fired a newsletter the moment a pack was published. I moved it to a manual admin button on the pack
page, because pressing publish often happens before the metadata or thumbnails are right, and a misfire goes out to every subscriber. Now publishing
is reversible-ish and sending is deliberate.

What it looks like

The visual identity carries most of the editorial feeling. Warm paper-cream background (#f4ecdc), ink-black body type, and a single vermilion accent
(#ed3d1e) used only on the wordmark’s trailing period, sale tags, hover states, and a slow-rotating six-pointed star in the header. Type is Fraunces
for display, Plus Jakarta Sans for body, JetBrains Mono uppercase with wide tracking for eyebrows and metadata. A subtle dotted grain texture
multiplies over the whole page so a flat WebP wallpaper preview reads as an object on a page rather than a tile in a grid. Every preview gets a thin
border and a “plate” shadow — the frame is part of the brand.

The voice doc I wrote for myself bans “unleash,” “elevate,” “transform,” and “discover,” and asks the copy to sound like a small studio writing to one
reader. “Hand-stitched in the browser” instead of “Crafted with passion.”

The stack, briefly

Next.js 15 App Router with React 19, Supabase Postgres + Auth + Storage with RLS on every table, Stripe Checkout with a webhook that writes purchases,
Resend for transactional email, Tailwind with custom tokens, and a tiny set of branding tokens in src/site.config.ts so the whole site can be
rebranded by editing one file. Netlify handles previews per branch and the public env vars let me retarget any preview at a different Supabase or
Stripe project without a code change.

What I’d do differently

I’d lean even harder on database-level guarantees. The exactly-one-item check has paid for itself several times over; I’d love to add similar
constraints around entitlement freshness and cap a few of the admin-facing tables with stricter row-count limits before the project grows. I’d also
start the manual-newsletter design from day one — moving it from automatic to manual was a small refactor, but it would have been zero work if I’d
never bound publish-and-send together in the first place.

Leave a Reply

Discover more from Sean Blowers

Subscribe now to keep reading and get access to the full archive.

Continue reading