NDA Client Platform

Private Directory Platform

I was hired by a design studio to build a private full-stack directory platform for one of their clients. Because the project is private and covered by confidentiality expectations, the studio name, client name, brand, industry, and domain are intentionally omitted.

Project TypePrivate client platform
RoleFull stack engineering, data pipeline, UI/UX
StackNext.js, React, TypeScript, Neon, Python, Cloudflare

The work went far beyond a static marketing site. I built the data pipeline, cleaned and imported location records into Neon, designed the database-facing API layer, created authentication and account flows, built public profiles and organization pages, connected map search to live data, configured Cloudflare bot protection, integrated Cloudflare Images for uploads, and designed the UI/UX across the product.

The core challenge was turning messy third-party directory data into a reliable product database, then building a user-facing application on top of it. Imported records needed normalized addresses, phone numbers, slugs, hours, feature tags, duplicate handling, correction workflows, coordinates, and image records.

On the application side, I separated API routes, repositories, server-only services, and UI components so the platform could support search, detail pages, map data, profile pages, image uploads, moderation, admin review flows, and analytics without pushing sensitive logic into the client.

My Role

What I Built

I owned the implementation across data engineering, backend APIs, account flows, map functionality, media handling, admin workflows, and the interface system.

  • Wrote Python scripts to scrape and import records from an online directory into a new Neon/PostgreSQL schema.
  • Cleaned inconsistent source data: whitespace, state names, phone numbers, duplicate slugs, address fragments, operating hours, pricing-style rows, and categorization fields.
  • Built Next.js API routes for search, detail data, map markers, media uploads, analytics, and supporting lookup data.
  • Implemented authentication, signed session cookies, account settings, profile pages, password reset, email verification, and staff/admin access paths.
  • Designed and built the product UI/UX, including homepage search, directory cards, detail pages, profile pages, admin screens, upload forms, and responsive navigation.
  • Integrated Cloudflare Turnstile, rate limiting, Cloudflare Images, image validation, upload metadata, moderation status, and delivery variants.

System Pieces

What Had To Work Together

The platform combined imported records, authenticated users, public pages, map data, image infrastructure, moderation rules, and admin review tools.

Data Import Pipeline

Python scripts pulled records from an external directory, normalized fields, generated stable slugs, and inserted related rows into Neon.

Search API

Next.js API routes accepted query, location, filter, and pagination parameters before handing normalized inputs to repository functions.

Authentication

The app used signed, HTTP-only session cookies backed by hashed session tokens stored in the database.

Profiles

Users could manage account data, display names, profile metadata, avatars, banners, saved items, posts, comments, and public profile views.

Organization Pages

Imported records became public detail pages with structured addresses, contact data, photos, comments, reviews, correction flows, and SEO metadata.

Map Integration

MapLibre rendered live location markers, while API routes returned bounded marker sets connected to the same directory data.

Cloudflare Media

Upload services validated file type and size, hashed image contents, sent files to Cloudflare Images, and stored delivery URLs with moderation metadata.

Trust And Admin Tools

Rate limits, Turnstile verification, moderation rules, duplicate reports, correction workflows, admin queues, and repair scripts protected data quality.

Data Pipeline

From Scraped Records To Product Data

The imported source data was useful, but not product-ready. A large part of the work was turning inconsistent records into stable relational data.

  • Mapped the old source shape into a cleaner relational model for accounts, sessions, organization records, addresses, phones, hours, media, reviews, comments, reports, moderation, and admin analytics.
  • Generated deterministic slugs and duplicate-safe identifiers so imported records could become stable public URLs.
  • Normalized phone numbers, state values, postal codes, operating hours, feature tags, payment-style metadata, and address strings before insert.
  • Added data repair scripts for duplicate merges, address-unit cleanup, missing coordinate backfills, primary media repair, and Cloudflare URL corrections.
  • Connected geocoding to the address model so map markers and location search could use cleaned coordinates instead of raw imported text.
  • Preserved review and moderation paths so future corrections could be audited instead of silently mutating production data.

Architecture

How The App Was Structured

The codebase separates route handlers, server-only services, repositories, UI components, and operational scripts so each system has a clear place to live.

  • Used Next.js App Router with server components, route handlers, server actions, repository modules, and server-only service files.
  • Kept database access behind typed repository functions using Neon serverless Postgres queries.
  • Separated media upload responsibilities into validation, Cloudflare upload, moderation decision, and persisted media asset creation.
  • Used bounded map APIs with coordinate validation, limit clamping, no-store headers, and rate-limit headers.
  • Built profile and account flows around database-backed sessions, email verification, password reset tokens, and role-aware admin paths.
  • Added sitemap, robots, analytics, and structured-data helpers so imported public records could be indexed and measured without hardcoding every page.

Code

Anonymized Implementation Snippets

These examples are rewritten from the real implementation patterns to show the engineering work without exposing client-specific names, data, or domain logic.

scripts/import_records.py (anonymized)

Normalizing Imported Directory Records

The import scripts cleaned inconsistent source data before inserting it into Neon, including phone normalization and duplicate-safe slug generation.

def normalize_phone(raw_phone: str | None) -> str | None:
    if not raw_phone:
        return None

    digits = re.sub(r"\D", "", raw_phone)
    if len(digits) == 11 and digits.startswith("1"):
        digits = digits[1:]

    return digits if len(digits) == 10 else None


def make_record_slug(name: str, city: str | None, region: str | None, used: set[str]) -> str:
    base = slugify(" ".join(part for part in [name, city or "", region or ""] if part))
    slug = base or "record"
    counter = 2

    while slug in used:
        slug = f"{base}-{counter}"
        counter += 1

    used.add(slug)
    return slug

src/app/api/directory/route.ts (anonymized)

Rate-Limited Search Endpoint

Public APIs parse user filters, enforce database-backed rate limits, and return normalized search results with rate-limit headers.

export async function GET(request: NextRequest) {
    const rateLimit = await enforceRateLimits(request, publicApiRateLimitPolicies.directorySearch);
    if (!rateLimit.allowed) {
        return rateLimitExceededResponse(rateLimit);
    }

    const { searchParams } = new URL(request.url);
    const result = await searchDirectoryRecords({
        q: searchParams.get("q") ?? undefined,
        region: searchParams.get("region") ?? undefined,
        city: searchParams.get("city") ?? undefined,
        claimed: searchParams.get("claimed") === "true",
        features: searchParams.getAll("feature"),
        page: Number(searchParams.get("page") ?? 1),
        limit: Number(searchParams.get("limit") ?? 24),
    });

    return withRateLimitHeaders(
        NextResponse.json({ success: true, data: result }),
        rateLimit
    );
}

src/lib/services/mediaUpload.ts (anonymized)

Cloudflare Image Upload Guardrails

Image uploads were validated, hashed, uploaded to Cloudflare Images, and stored with moderation metadata instead of trusting the raw form upload.

export async function uploadModeratedImage(params: UploadParams) {
    const validated = await validateImageFile(params.fileEntry);
    if (!validated) return null;

    const decision = params.user.role === "admin"
        ? null
        : moderateImageUpload({
            context: params.context,
            mimeType: validated.file.type,
            fileName: validated.file.name,
            fileSizeBytes: validated.fileSizeBytes,
            caption: params.caption ?? null,
        });

    const image = await uploadImageToCloudflare({
        file: validated.file,
        creatorUserId: params.user.id,
        metadata: { context: params.context, entityId: params.entityId },
    });

    return createMediaAsset({
        uploaderUserId: params.user.id,
        cloudflareImageId: image.id,
        imageUrl: getCloudflareDeliveryUrl(image.id, "public"),
        checksumSha256: validated.checksumSha256,
        moderationStatus: decision?.mediaStatus ?? "pending",
    });
}

src/app/api/homepage/map/route.ts (anonymized)

Viewport-Bound Map API

The map endpoint validates bounds, expands the viewport slightly, clamps limits, and returns only the marker data needed by the client.

function normalizeBounds(bounds: MapBounds): MapBounds {
    return {
        west: clamp(bounds.west, -180, 180),
        south: clamp(bounds.south, -90, 90),
        east: clamp(bounds.east, -180, 180),
        north: clamp(bounds.north, -90, 90),
    };
}

export async function GET(request: NextRequest) {
    const bounds = normalizeBounds(parseBounds(request.nextUrl.searchParams));
    const limit = clamp(Number(request.nextUrl.searchParams.get("limit") ?? 2000), 1, 2500);

    if (bounds.south >= bounds.north || bounds.west === bounds.east) {
        return NextResponse.json({ success: false, message: "Invalid map bounds." }, { status: 400 });
    }

    const markers = await getDirectoryMarkersInBounds(expandBounds(bounds), limit);
    return NextResponse.json({ success: true, data: { markers, limit } });
}

Outcome

What This Project Shows

The public version of the case study is intentionally constrained, but the technical scope still shows the kind of full-stack product work involved.

  • Built a production-grade private client project that demonstrates full-stack engineering beyond portfolio-scale prototypes.
  • Turned messy imported directory data into a searchable, geocoded, media-enabled product database.
  • Connected data engineering, backend APIs, authentication, map UX, media infrastructure, moderation, and interface design into one cohesive platform.
  • Kept the public case study technically specific while protecting the studio, client, brand, vertical, repo, and domain.