Skip to content
mouaad.sek:~$
All case files~/work/cf-01$
CF-01Product · LiveSTATUS: IN PRODUCTION

Travistello

A product decision platform where every score is derived from a versioned model and cited sources, and where trust rules are enforced in the code, not in a disclaimer.

ROLE
Solo, from design to production
WHEN
2026
STACK
Next.js 16, React 19, TypeScript, Payload 3, Neon Postgres, Three.js, Vercel, Vitest, Playwright
01 / CONTEXT

The problem

Most gear review sites hide how their scores are made, mix "we tested it" with "we read about it", and send clicks through redirects anyone can abuse.

Travistello is the opposite bet: a catalog where every product, variant, spec, source and score is a typed record, and where the platform itself refuses to publish anything it cannot back up.

02 / APPROACH

From raw evidence to a click

  1. 01Product + variant
  2. 02Evidence sources
  3. 03Versioned score
  4. 04Offer
  5. 05Compare
  6. 06Tracked redirect
03 / TRUST AND SECURITY RULES IN THE CODE

Trust and security rules in the code

RULELAYERPREVENTS
Opaque /go/[token] redirects with server-owned destinationsRoutingOpen redirect
Outbound clicks store no IP address, bots are filtered outAnalyticsTracking abuse
Score weights must total 100%, the overall score is derivedData modelHand-tuned scores
Research-based and hands-on reviews are distinct data statesData modelFake reviews
Every media record needs rights owner and usage basisCMSLicensing issues
Affiliate programs and click data restricted to adminsCMS rolesData exposure
04 / EVIDENCE

Straight from the code

Excerpts from the actual repository and deliverables, trimmed for length.

src/lib/outbound.tstypescript
export const getSafeOutboundURL = (value: null | string | undefined) => {
  if (!value) return null;

  try {
    const url = new URL(value);

    if (
      !["http:", "https:"].includes(url.protocol) ||
      url.username ||
      url.password
    )
      return null;

    return url;
  } catch {
    return null;
  }
};

Every outbound link goes through this check: only http(s), never embedded credentials. Destinations come from the database, never from the URL.

src/app/go/[token]/route.tstypescript
export async function GET(request: NextRequest, { params }: RouteContext) {
  const { token } = await params;
  const offers = await payload.find({
    collection: "offers",
    limit: 1,
    where: {
      and: [
        { publicToken: { equals: token } },
        { status: { equals: "active" } },
      ],
    },
  });
  const offer = offers.docs[0];

  if (!offer || !isPublicOffer(offer))
    return new NextResponse("Offer not found.", { status: 404 });

  const destination = getSafeOutboundURL(offer.affiliateUrl || offer.destinationUrl);
  if (!destination)
    return new NextResponse("Offer destination is unavailable.", { status: 503 });

  const bot = isLikelyBot(request.headers.get("user-agent"));
  // ... only non-bot clicks are recorded, with no IP address
}

The /go/[token] redirect: the token is looked up server-side, inactive offers return 404, and bots never create click records.

05 / OUTCOME

Results

28products published across 5 pillars
6comparison sets
3delivery slices, all live and verified
06 / LESSONS

What I'd do differently

The hard part was never the features, it was the trust rules: deciding what the platform must refuse to show. Next: activate affiliate programs and add the first hands-on reviews as their own data state.

NEXT CASE FILE · CF-02Supply chain security on Kubernetes
Esc

↑↓ navigate · Enter open · Esc close