[CODE]

Designing CastLoop: A Fishing Community Platform

I wanted a place where local anglers help traveling fishermen — sharing gear, spots, and local knowledge. Here is how I thought through the product and the tech stack decisions that followed.

6 min read
product nextjs typescript software-engineering

Every time I travel to fish somewhere new, I run into the same problem. I don’t know the spots. I don’t know what gear works there. And I definitely don’t want to buy a whole new setup just for a weekend trip. I always end up spending hours on fishing forums, hoping someone local will reply to my post before I leave.

That’s the itch CastLoop is trying to scratch.

The idea is pretty simple: anglers who know a place help anglers who are passing through. Lend them gear, share a spot or two, maybe even meet up and fish together. There are platforms that do this for surfboards and camping equipment, but nothing I’ve found built specifically around fishing culture.

What the Product Actually Needs to Do

Before picking any tech, I spent some time writing out what the product actually has to do — not features, just jobs.

The platform centers on four types of help that anglers can offer each other. I ended up modeling these as distinct Cast types:

Gear lending (borrow_gear). A local angler posts what they’re willing to lend and when. A traveling angler browses, requests, and coordinates pickup. Simple enough, but there’s real trust involved — someone is handing over a rod that cost them NT$8,000.

Fishing tips (fishing_tip). Local knowledge about techniques, rigs, and target species for a specific area. Not just coordinates — the context that makes the spot worth visiting.

Find a buddy (find_buddy). Some people fish better with company, or want a local guide who knows the water. This lets anglers arrange to fish together.

Beginner guidance (beginner_guide). A category specifically for helping newcomers. Experienced anglers who want to give back can post structured advice for people just getting started.

Messaging. Everything else falls apart without a way for people to actually talk. Gear exchanges need coordination, buddy arrangements need planning. So a basic messaging system is non-negotiable.

Reputation points. Handing over expensive gear to a stranger requires trust, and trust has to be earned. The platform tracks total_points and help_count per user, with a point transaction log behind every helpful action. This isn’t gamification for its own sake — it’s the mechanism that lets borrowers know whether a lender is reliable and vice versa.

That’s the core. I tried hard not to scope-creep my way into building Airbnb for fishing on day one, but the reputation layer isn’t optional — without it, nobody lends anything.

Why PostgreSQL Instead of SQLite

My first instinct was SQLite. It’s zero-config, works great locally, and for a hobby project it’s usually more than enough.

But this is a community platform. Multiple users are writing at the same time — someone is submitting a gear request while another person is posting a review. SQLite’s write locking becomes a problem fast under concurrent writes. PostgreSQL handles that properly.

I ended up hosting on Railway. One thing I learned the hard way: Prisma works better with a persistent database connection than the serverless model. Railway gave me a real PostgreSQL instance I can connect to without worrying about cold-start connection pooling headaches at the free tier.

The Prisma Workflow I’m Using

Rather than writing types by hand, I pull them directly from the database schema:

npx prisma db pull && npx prisma generate

This generates types/database.ts automatically. I never touch that file manually. If the schema changes, I update schema.prisma, run a migration, and regenerate.

// types/database.ts — auto-generated, never edit by hand
export type Prisma = {
  gear: {
    id: string;
    title: string;
    ownerId: string;
    available: boolean;
    createdAt: Date;
  };
  // ...
};

It sounds obvious, but it took me a few projects to stop fighting with type drift between my Prisma schema and hand-written interfaces. Just let Prisma generate everything and move on.

DDD Kept the Layers Honest

I applied Domain-Driven Design with a strict rule: the domain layer has zero framework imports. No Prisma types leaking into domain entities. No Next.js server action logic sitting inside a domain service.

The boundary looks like this:

// domain/gear/GearListing.ts — no Prisma, no Next.js
export type GearListing = {
  id: GearListingId;
  title: string;
  ownerId: UserId;
  isAvailable: boolean;
};

// infra/prisma/GearListingMapper.ts — Prisma lives here
import type { gear } from "@prisma/client";
import type { GearListing } from "@/domain/gear/GearListing";

export const toGearListing = (row: gear): GearListing => ({
  id: row.id as GearListingId,
  title: row.title,
  ownerId: row.ownerId as UserId,
  isAvailable: row.available,
});

The mapper is the only place that knows about both layers. Server actions call app services, app services call repositories, repositories speak Prisma. It’s a bit more ceremony than just calling Prisma directly in a server action, but the layers stay clean and testable.

Auth Was Not Worth Rolling Myself

I spent maybe two hours considering whether to build email/password auth from scratch. Then I remembered every past project where I did that and hit an edge case with token expiry or email verification three weeks later.

Auth.js handles email magic links and Google OAuth. I configured it once:

// auth.config.ts
export const authConfig = {
  providers: [
    Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    Resend({ from: "no-reply@castloop.app" }),
  ],
  // ...
};

Done. Moving on. The product isn’t about auth innovation.

Next.js App Router for the Right Reason

I picked Next.js 14 App Router not because it’s trendy but because server components genuinely reduce the amount of JavaScript I’m sending to the browser. A gear listing page doesn’t need to be interactive — it just needs to render fast. With server components, I can fetch from the database and render HTML without a client bundle for that page.

The tradeoff is that the mental model takes getting used to. "use client" boundaries, async server components, when data fetching happens — it took me a while to stop accidentally making things client-side that didn’t need to be.

But for a content-heavy community platform where most pages are read-heavy, the architecture fits the use case well.