# Local City Places Case Study — Carter Steinhoff

> Local City Places case study: a Next.js directory and membership rewards platform with merchant self-service pages, Stripe billing, and role-based dashboards on Postgres.

- Carter's role: Product design & full-stack development

- System: Next.js · Drizzle · Neon Postgres · Stripe · Vercel

- Status: Live and in production

- [Live site](https://localcityplaces.com/)

## Overview

A directory that makes category exclusivity operational.

Local City Places pairs a public Phoenix-metro directory with a scarce membership model: each city-and-category slot belongs to one merchant. I designed the resident experience and the request, waitlist, invitation, listing, and billing workflows that make that promise operable.

The hard part was not displaying businesses. It was keeping category availability, first-come priority, ownership, membership, and public listings consistent as residents, merchants, and administrators act on the same records.

## Project story

### Marketplace: Every request changes what the directory can offer.

Residents browse local businesses; merchants request exclusive category positions; administrators coordinate invitations, listings, and billing. Once only one slot is available, every request can affect availability and another merchant’s place in line.

### Queue: Exclusivity needs a queue, not an inbox.

A slot may be open, requested, invited, filled, or waitlisted before a merchant ever has an account. Each state has to remain understandable to the next applicant and the team operating the directory.

### Operating model: Every role works from the same operating model.

An accepted request can move through invitation, listing ownership, billing, and publication without being re-entered at each stage. Each role gets the controls needed to move it forward.

## Decisions

### Category priority is an index, not a promise

Category requests are timestamped and indexed by city, state, category, and creation time. The admin queue resolves first-come priority from data instead of inbox history or judgment.

### A business exists before its owner does

Requests, invitations, and merchant accounts remain separate. A listing can exist before ownership is claimed, then gain users and billing when an invitation is accepted.

### The daily entry cap is a unique index

A unique index on member and Arizona entry date enforces one sweepstakes entry per day, including concurrent requests. A plain local date avoids DST conversion.

### One identity, three dashboards, no passwords

Members, merchants, and admins share one passwordless user model with role-specific views. Listings, category positions, and member activity therefore remain in the same schema.

## Architecture

One schema, one directory, three dashboards.

A single Next.js application serves the public directory and all three dashboards, with a typed Postgres schema underneath so merchants, members, offers, and campaigns stay consistent across every surface.

- **Experience:** Next.js App Router, React, TypeScript, Tailwind, and Radix primitives
- **Application layer:** Role-based admin, merchant, and member dashboards with magic-link auth
- **Data:** Drizzle ORM over Neon Postgres covering merchants, members, offers, and reviews
- **Operations:** Stripe billing, transactional and campaign email, Vercel delivery

## Technology and delivery

A typed product stack for a public directory and three dashboards.

The directory, role-based dashboards, merchant publishing, membership loop, and billing run in one Next.js application over a shared PostgreSQL model.

### Experience

- **nextjs:** Public directory and role-based application routes
- **react:** Resident, merchant, member, and admin interfaces
- **typescript:** Typed contracts across UI, actions, and data
- **tailwind:** Responsive product design system

### Data, commerce & delivery

- **drizzle:** Schema, migrations, and typed data access
- **neon:** Managed PostgreSQL for merchants, requests, members, offers, and history
- **stripe:** Merchant subscription and billing workflows
- **vercel:** Application, server routes, and asset delivery

## What Carter owned

- Product design and full-stack Next.js development across the public site and all three dashboards
- Role-based admin, merchant, and member areas on one Postgres schema
- Timestamped category-request queue with waitlisting, invites, and merchant onboarding
- Passwordless magic-link authentication and a logged email campaign system

## Selected screens

- **Merchant directory:** The public directory leads with real merchants, their city, and their category rather than a search box. [Image](https://cartersteinhoff.co/images/local-city-places-project.webp) — Local City Places homepage with a grid of Phoenix metro merchants including restaurants, nurseries, and services
- **Founding merchant request:** A four-step request flow uses address autocomplete and timestamps each submission so category priority is first come, first served. [Image](https://cartersteinhoff.co/images/local-city-places-request.webp) — Local City Places founding merchant request form showing step one of four with category and business address fields
- **The category queue:** Two Tempe dining requests, a week apart: the first is fulfilled, the second waitlisted. Priority is decided by submission time, not by who follows up hardest. [Image](https://cartersteinhoff.co/images/local-city-places-requests.webp) — Local City Places admin merchant requests table showing seven businesses with categories, submission dates, and statuses including one waitlisted
- **What the merchant buys:** The merchant dashboard presents category exclusivity alongside the included campaign channels. [Image](https://cartersteinhoff.co/images/local-city-places-marketlock.webp) — MarketLock360 page in the merchant dashboard headlined Lock in your city, listing eight growth channels, 5,000 homes mailed monthly, and radio airplay
- **Member nominations:** Members can see their nomination status and referral link from one dashboard. [Image](https://cartersteinhoff.co/images/local-city-places-member.webp) — Local City Places member dashboard showing sweepstakes cycle entry status, a referral link, and a leaderboard of five members
- **Public merchant page:** A live profile brings gallery, hours, services, contact details, and merchant media into one public record. [Image](https://cartersteinhoff.co/images/local-city-places-merchant.webp) — Local City Places profile for R Robinson Bookkeeping with a photo gallery, Phoenix location, business details, hours, and overview

## Current status

Live in production; these screens are captured from the current build.

- [Back to all case studies](https://cartersteinhoff.co/portfolio/index.md)

- [Discuss a similar project](https://cartersteinhoff.co/contact/index.md)
